Run Babel transpiler on Odoo's javascript bundle

Odoo web client default framework is based on a combination of jQuery and BackboneJS. Now, jQuery is old but it is not necessary worse. Technologies serve its purpose as long as it works and meet requirements. However, there is cases when non-product requirements need to be considered such as when your development team is heavily invested on, say ReactJs, and productivity can be greatly improved when the familiar toolset is available. Needless to say, productivity means cost saving. Thus, this post quickly looks into necessary steps to integrate Babel transpiler with Odoo’s asset bundling.

Web clients in Odoo have their asset bundled and minified in production. The bundler supports css preprocessors like Sass and Less right out of the box. Support for javascript transpiler like Babel was not built-in. Fortunately, supporting Babel transpiling can be achieved via sub-classing the default bundler.

The first step is to let QWeb module knows that it should use our custom asset bundler clas. The below code inherit QWeb model to create AssetBundleJsx instead of the default class.

class IrQweb(models.AbstractModel):
    """ Add ``raise_on_code`` option for qweb. When this option is activated
    then all directives are prohibited.
    """
    _inherit = 'ir.qweb'

    def _get_asset_bundle(self, bundle_name, files, env=None, css=True, js=True):
        return assetsbundle.AssetsBundleJsx(bundle_name, files, env=env, css=css, js=js)

AssetsBundleJsx is our custom class that looks for jsx assets and pre-process them with Babel before bundling. Since there is no readily available Python binding for Babel, we will make do with a subprocess call to babel command.

# install babel command line
npm install -g @babel/core @babel/cli @babel/preset-react

The snippet below for AssetBundleJsx is pretty self descriptive. There is one caveat that the inclusion order of js files in the output bundle is not maintained. If this is a big deal, further work can be done to explore running Babel on every javascript in place of Odoo’s minify function. (And be prepared to deal with strict mode warnings).

def transpile_jsx(content_bundle):
    npm_root = subprocess.run(['npm', '-g', 'root'], text=True, capture_output=True)
    command = ['babel', '--presets', npm_root.stdout + '/@babel/preset-react', '--no-babelrc']
    try:
        compiler = Popen(command, stdin=PIPE, stdout=PIPE,
                         stderr=PIPE)
    except Exception:
        raise CompileError("Could not execute command %r" % command[0])
    (out, err) = compiler.communicate(input=content_bundle)
    if compiler.returncode:
        cmd_output = misc.ustr(out) + misc.ustr(err)
        if not cmd_output:
            cmd_output = u"Process exited with return code %d\n" % compiler.returncode
        raise CompileError(cmd_output)
    return out

class JsxAsset(JavascriptAsset):
    @property
    def content(self):
        # print('jsx asset content')
        content = super().content
        content = transpile_jsx(content.encode('utf-8')).decode('utf-8')
        print (content)
        return content

    

class AssetsBundleJsx(AssetsBundle):

    def __init__(self, name, files, env=None, css=True, js=True):
        super(AssetsBundleJsx, self).__init__(name, files, env=env, css=css, js=js)
        for idx, js in enumerate(self.javascripts):
            # only run transpiler on our own custom script.
            # In production, a better way to distinguish jsx
            # script is needed. 
            if js.url.find('todo') >= 0:
                self.javascripts[idx] = JsxAsset(self, url=js.url, filename=js._filename, inline=js.inline)

With that, Babel transpiler would run on every Odoo's javascript bundle before serving to client.

💡