Skip to content

Howto dev

den_po edited this page Jun 12, 2021 · 6 revisions

Vivaldi internals

Vivaldi is built on top of React - an UI creating library. Vivaldi source code consists of lots of modules combined by Webpack into a few js-files. VivaldiHooks provides tools for replacing React-components and modules' exports, giving mod developers a way to change not only the look but also the behavior hidden deep inside.

There is a project which may help research Vivaldi internals: bundleutils. In order to set it up you need to install Node.js and then execute the following command from the project's directory:

npm install

To do the work, copy original Vivaldi js files into the folder and run

node split.js

It creates folders with a modules saved to separate files, most of them have meaningful names. Inside the files arguments of require function (minified name in the most cases is n or a) - module numbers - are complemented by comments with module names if found. Example:

var i = a(0 /* React_wrapper */),
    s = a(10 /* PageActions */),
    n = a(2 /* _getLocalizedMessage */),
    r = a(126 /* settings_keywordFound */),
    o = a(97),

If you use source code editor with refactoring abilities it may be useful rename such variables into module names - modules after that are easier to understand

}), d(this, "openPrivacyPage", e => {
    PageActions.a.openURL(u), e.preventDefault()
}), this.state = {

Also if some module exports svg image the image is saved as a separate svg file.

So file names contain names which may be used by mods to reuse or modify modules. For examples presence of file jsout_name/React.js means that a mod may use "React" module:

const ReactModule = vivaldi.jdhooks.require("React")
//...
return ReactModule.createElement("div", {}, "Text")

or modify it

vivaldi.hookModule("React", //..

It's a little bit harder with React-classes.

class n extends React.PureComponent {
    render() {
        const {
            children: e,
            filter: t
        } = this.props;
        return t ? Array.isArray(e) ? e.map((e, a) => React.createElement(React.Fragment, {
            key: a,
            __source: {
                fileName: "C:\\new-bot\\new-builder\\workers\\ow32\\build\\vivaldi\\vivapp\\src\\components\\settings\\SettingsSearchCategoryChild.jsx",
                lineNumber: 18
            }
        }, Object(s.a)(e, t))) : Object(s.a)(e, t) : e
    }
}

VivaldiHooks uses fileName value to identify classes. Value part between "component" and ".jsx" with all \\ and / replaced by _ is used as a class name. In the example above class name would be "settings_SettingsSearchCategoryChild". It must be noted it's more often fileName is placed outside of the class, in that case it's defined as a variable before the class definition. Example:

var i = a(117 /* common_Favicon */),
    s = a(0 /* React_wrapper */),
    n = a.n(s),
    r = a(13 /* _VivaldiIcons */),
    o = "C:\\new-bot\\new-builder\\workers\\ow32\\build\\vivaldi\\vivapp\\src\\components\\tabs\\LoadingFavicon.jsx";
class l extends n.a.PureComponent {

When a module contains several React-classes it may be not so easy to understand which filename variable describes one or another class, so use refactoring to make it clearer.

Hooks API

addStyle

vivaldi.jdhooks.addStyle(style, /*optional*/ description)

Used to add css styles directly from js mod.

vivaldi.jdhooks.addStyle(`
	.quick-command:not([data-selected]) .quick-command-close-tab { display: none }
	.quick-command[data-selected] .quick-command-close-tab { background-color: rgba(0, 0, 0, .15)}
`, "qc-close-tab.js")

Optional description parameter is put to "description" argument of style tag in the page's header.

hookClass

vivaldi.jdhooks.hookClass(className, /*class => newClass*/ callback)

The main function for overriding React-classes. callback function receives old (or overrided by another mod) class as argument and should return a new/updated one.

vivaldi.jdhooks.hookClass("find-in-page_FindInPage", cls => {
	class newFindInPage extends cls {
		constructor(...e) {
			super(...e)

			this.populateWithPageSelection = () => { this.focusFindInPageInput() }
		}
	}
	return newFindInPage
})

hookModule

vivaldi.jdhooks.hookModule(moduleName, /*(moduleInfo, exports) => newExports*/ callback)

It calls callback after moduleName is initialized, allowing to override module's exports to change its behavior. callback should return new/overriden exports. Example: adding new default value for settings parameter:

vivaldi.jdhooks.hookModule("vivaldiSettings", (moduleInfo, exports) => {
    let oldGetDefault = exports.getDefault
    exports.getDefault = name => {
        switch (name) {
            case "BOOKMARK_BUTTON_POSITION": return "value"
            default: return oldGetDefault(name)
        }
    }
    return exports
})

`hookModule` is deprecated and will be removed in the nearest future

vivaldiSettings module exports an object implementing several functions, one of them - getDefault - returns default value for a specified parameter.

require

vivaldi.jdhooks.require(moduleId, exportName)

It returns exports of specified module. It accepts both integer value as module number (may be useful for debugging) and string value as module name. This is the main way to reuse Vivaldi components.

const UrlFieldActions = vivaldi.jdhooks.require('_UrlFieldActions')

UrlFieldActions.go(["https://vivaldi.com"], {
	inCurrent: false,
	addTypedHistory: true,
	addTypedSearchHistory: false,
	enableSearch: true
})

Sometimes a module exports several entities (i.e. several different modules combined by ModuleConcatenationPlugin webpack-plugin). exportName parameter allows to choose which of them is required. Default value is "default".

If exported value is not an object the parameter is ignored and require returns the value as is.

If exportName is equal to "*" require returns exported value as is.

If exportName matches one of exported object's properties name require returns the property.

If exportName is equal to "default" (or not specified) and a module exports an object with a single property require returns the property.

Warning: require may be called only from VivaldiHooks API callbacks or from functions called only from these callbacs. This is because require forces module's dependencies to be initialized, so they may be loaded before mods calling hookModule for them.

insertWatcher

vivaldi.jdhooks.insertWatcher(class, /*{settings:[], prefs:[]}*/ params)

Extends class to make it watch for settings/prefs changes. Settings (see module vivaldiSettings) will be available through this.state.jdVivaldiSettings.{keyname}, prefs (PrefsCache) - this.state.jdPrefs["{keyname}"]. Example:

return vivaldi.jdhooks.insertWatcher(downloadTabWebpageContent,
    {
        settings: ["SHOW_DOWNLOADTAB_FOR_NEW_DOWNLOADS"],
        prefs: ["vivaldi.tabs.visible", "vivaldi.clock.mode"]
    })

Inside class:

const val1 = this.state.jdVivaldiSettings.SHOW_DOWNLOADTAB_FOR_NEW_DOWNLOADS
const val2 = this.state.jdPrefs["vivaldi.tabs.visible"]

Don't pass function's result to React.createElement directly, store it as a constant outside of render or even your hooked class. Use _PrefKeys module to access pref names.

onUIReady

vivaldi.jdhooks.onUIReady(callback)

Legacy method used for old style mods working through DOM event listeners. callback is called after Vivaldi UI is initialized and mounted to DOM.

vivaldi.jdhooks.onUIReady(() => {
	document.addEventListener("mouseup", event => {

React

React works with Virtual DOM, initialized components may not exist in the real DOM for some time after rendering. Take a look at React component lifecycle explanation.

DOM in React-application is a bunch of nested into each other elements created by React.createElement call.

render() {
    return React.createElement(settings_SettingsSearchCategoryChild, { filter: this.props.filter },
        React.createElement("h2", null, "Hooks"),
        React.createElement("div", { className: "setting-group unlimited" },
            React.createElement("div", { className: "setting-single" },
                React.createElement("label", null,
                    React.createElement("input", {
                        type: "checkbox",
                        checked: this.state.defaultLoad,
                        onChange: this.toggleDefaultLoad.bind(this)
                    }),
                    React.createElement("span", null, "Startup mode for new items")
                )
            ),
            React.createElement("div", {
                className: "setting-group unlimited pad-top"
            },

The most demonstrative way to modify React-class is changing result of parent class' render call. For that result's .props.children value should be modified. I recommend to hook some React class and override its render this way to learn how it looks like in the console:

render()
{
    let r = super.render() //call original render
    console.log("new render", {this: this, rendered: r})
    return r
}

When you implement componentDidMount or componentWillUnmount in overridden class check existence of these method in the parent class and call them if they exist. It will help avoiding bugs if two mods implement these methods for the same class.

componentWillUnmount() {
    //do some work
    //...
    if (super.componentWillUnmount) super.componentWillUnmount()
}

Sometimes class methods are defined not statically (as class members) but dinamically at runtime (as newly created object's members). You can override such methods only at runtime. Here is a part of original class implementation using this way (it looks ugly because js-beautify haven't managed to format it better):

}), fe(this, "attachEventHandlers", () => {
    q.a.addChangeListener(this._onWebpageviewStoreChanged)
}), fe(this, "removeEventHandlers", () => {
    q.a.removeChangeListener(this._onWebpageviewStoreChanged)
}), fe(this, "attachWebviewHandlers", () => {
    Object.keys(_e).forEach(e => {

fe in this example is a wrapper around Object.defineProperty. Here is an example of overriding a class member defined at runtime:

class newClass extends oldClass {
    constructor(...e) {
        super(...e)

        const old_handleLoadStop = this.handleLoadStop
        this.handleLoadStop = e => {
            //do some work
            //...
            return old_handleLoadStop(e)
        }

If your render implementation depends of values original render doesn't depend of (f.e. you want to display current time in a component), it's good to store these values to component's state and update them via this.setState, otherwise your component may not see value changes and it will not be refreshed when needed.

Modules

While split.js can split bundles onto separate files and reformat js code it cannot analyze modules' exports. For quick analyzis you may use dev-dumpVivaldi.js mod which defines vivaldi.jdhooks.dumpVivaldi() function. The function returns modules' exports sorted by categories and prettified in some cases. For example to check functions working with settings you may find and open elements objects and vivaldiSettings inside it.

Clone this wiki locally