This gist describes our current work at using native JS modules in Odoo, and what it implies for the source code and for the developer experience.
PR: odoo/odoo#63177 (still work in progress)
The current PR allows us to use native JS modules in odoo. It works. It is backward-compatible.
The benefits are:
- autocompletion across JS files
- intellisense (docstring are displayed when typing function, ...)
- ease of refactoring (IDEs can jump to symbol definition, can rename across files, ...)
- new developers will not need to learn odoo.define semantics
Other changes:
- we can no longer serve static files, so we always use a big bundle, even in debug=assets
- in debug=1 and debug=assets, bundle is non minified
- in debug=assets, we generate sourcemaps to make the debugging experience nicer
- it is faster: in debug=assets, we go from 5.86s/900requests to 3.27s/300 requests (cold start). Even faster if we do not need to regenerate sourcemaps.
What this PR does is converting a file written in the new style into a old-style module. So, for example:
/** @odoo-module **/
import { someFunction } from './some_file';
export function otherFunction(val) {
return someFunction(val + 3);
}
will be rewritten into something like:
/********************************************************
* Filepath: /web/static/src/some_file.js *
* Bundle: web.assets_backend *
* Lines: 448 *
********************************************************/
odoo.define('@web/some_file', function (require) {
'use strict';
let __exports = {};
const { someFunction } = require("web.ActionModel");
__exports.otherFunction = function otherFunction(val) {
return someFunction(val + 3);
};
return __exports;
};
So, as you can see, the transformation is basically adding odoo.define
on top, and updating the import/export statements. Also, note the first line comment: it describes that this file should be converted. Any file without that comment will be kept as-is.
But then, the original file is no longer a standalone JS file that can be included in a script tag like we did so far. We can no longer simply load them in the browser in debug=assets. So, what we do is then serve the bundle, but non minified.
With this PR, we then create two versions for each bundle. For example, assets_backend.min.js
and assets_backend.js
. The minified version is the version where all comments and (useless) spaces have been removed, and will be served for normal usecases. The non minified version is only generated and used in debug=assets mode. It is really the same file, except that we keep all the comments/spaces.
We also generate sourcemap files for the non minified file.
To help with backward compatibility, and also to avoid breaking code, we also provide a way to define an alias. If the comment tag on top of the file looks like this:
/** @odoo-module alias=web.AbstractAction **/
The generated bundle will also export an additional module, named web.AbstractAction
:
odoo.define(`web.AbstractAction`, function(require) {
return require('@web/js/chrome/abstract_action')[Symbol.for("default")];
});
Using alias makes sure that all existing code importing the previous JS module will still work. Note that it is unclear if that feature will be removed in the future with a deprecation warning, or it if will stay as is.
Note that the normal behaviour of alias modules is to export the default value
of the module. This is necessary, because many old
odoo modules simply returned
a value that was then imported by other module. It was therefore a default value.
However, sometimes, we want the aliased module to behave exactly like the original
module. In that case, we can just use the default
argument like this:
/** @odoo-module alias=web.AbstractAction default=0**/
This will define an alias with exactly the values exported by the original module:
odoo.define(`web.AbstractAction`, function(require) {
return require('@web/js/chrome/abstract_action');
});
Each file/module has an official
name: web/static/src/file_a.js
has the name
@web/file_a
. So, to import that file, one can simply use the following
code:
import {something} from `@web/file_a`
Relative imports work, but only inside an odoo addon. So, imagine that we have the following file structure:
addons/
web/
file_a.js
file_b.js
stock/
file_c.js
The file file_b
can import file_a
like this:
import {something} from `./file_a`
But file_c
need to use the full name:
import {something} from `@web/file_a`
Note that we will provide a way to generate a tsconfig.json file to help IDE make the mapping between those complete name and the actual file it refers to, so we can still benefits from intellisense.
We probably should mention the alias system ?