Skip to content

Style Guide

This guide documents how to use the official GJS ESlint configuration, as well as other preferred styles that can't be expressed by a linter configuration.

It also includes a basic introduction to setting up a project to use .eslintrc.yml and .editorconfig files, to help reduce manual work for developers.

ESLint

TIP

GNOME Shell includes one additional global variable called global.

ESLint is a well known linter and static analysis tool for JavaScript, used by both GJS and GNOME Shell. It's used by many projects to maintain code quality, enforce coding standards, catch potential errors, and improve code consistency.

The recommended configuration includes rules for static analysis, deprecated syntax and a list of all the global variables for the environment. Put the .eslintrc.yml in the root of your project directory, and your IDE will provide real-time diagnostics and warnings.

.eslintrc.yml
yml
# SPDX-License-Identifier: CC0-1.0
# SPDX-FileCopyrightText: No rights reserved

env:
  es2021: true
extends: 'eslint:recommended'
rules:
  # See: https://eslint.org/docs/latest/rules/#possible-problems
  array-callback-return: error
  no-await-in-loop: error
  no-constant-binary-expression: error
  no-constructor-return: error
  #no-duplicate-imports: error
  no-new-native-nonconstructor: error
  no-promise-executor-return: error
  no-self-compare: error
  no-template-curly-in-string: error
  no-unmodified-loop-condition: error
  no-unreachable-loop: error
  no-unused-private-class-members: error
  no-use-before-define:
    - error
    - functions: false
      classes: true
      variables: true
      allowNamedExports: true
  # See: https://eslint.org/docs/latest/rules/#suggestions
  block-scoped-var: error
  complexity: warn
  consistent-return: error
  default-param-last: error
  eqeqeq: error
  no-array-constructor: error
  no-caller: error
  no-extend-native: error
  no-extra-bind: error
  no-extra-label: error
  no-iterator: error
  no-label-var: error
  no-loop-func: error
  no-multi-assign: warn
  no-new-object: error
  no-new-wrappers: error
  no-proto: error
  no-shadow: warn
  no-unused-vars:
    - error
    - varsIgnorePattern: ^_
      argsIgnorePattern: ^_
  no-var: warn
  unicode-bom: error
  # GJS Restrictions
  no-restricted-globals:
    - error
    - name: Debugger
      message: Internal use only
    - name: GIRepositoryGType
      message: Internal use only
    - name: log
      message: Use console.log()
    - name: logError
      message: Use console.warn() or console.error()
  no-restricted-properties:
    - error
    - object: imports
      property: format
      message: Use template strings
    - object: pkg
      property: initFormat
      message: Use template strings
    - object: Lang
      property: copyProperties
      message: Use Object.assign()
    - object: Lang
      property: bind
      message: Use arrow notation or Function.prototype.bind()
    - object: Lang
      property: Class
      message: Use ES6 classes
  no-restricted-syntax:
    - error
    - selector: >-
        MethodDefinition[key.name="_init"]
        CallExpression[arguments.length<=1][callee.object.type="Super"][callee.property.name="_init"]
      message: Use constructor() and super()
# GJS Globals
globals:
  ARGV: readonly
  Debugger: readonly
  GIRepositoryGType: readonly
  globalThis: readonly
  imports: readonly
  Intl: readonly
  log: readonly
  logError: readonly
  pkg: readonly
  print: readonly
  printerr: readonly
  window: readonly
  TextEncoder: readonly
  TextDecoder: readonly
  console: readonly
  setTimeout: readonly
  setInterval: readonly
  clearTimeout: readonly
  clearInterval: readonly
  # GNOME Shell Only
  global: readonly
  _: readonly
  C_: readonly
  N_: readonly
  ngettext: readonly
parserOptions:
  ecmaVersion: 2022
  sourceType: module

ESLint is transitioning to a new flat configuration that uses ES Modules. To use this configuration, be sure your project has a package.json file with "sourceType": "module".

eslint.config.js
js
// SPDX-License-Identifier: CC0-1.0
// SPDX-FileCopyrightText: No rights reserved

import js from '@eslint/js';

export default [
    js.configs.recommended,
    {
        languageOptions: {
            globals: {
                ARGV: 'readonly',
                Debugger: 'readonly',
                GIRepositoryGType: 'readonly',
                globalThis: 'readonly',
                imports: 'readonly',
                Intl: 'readonly',
                log: 'readonly',
                logError: 'readonly',
                pkg: 'readonly',
                print: 'readonly',
                printerr: 'readonly',
                window: 'readonly',
                TextEncoder: 'readonly',
                TextDecoder: 'readonly',
                console: 'readonly',
                setTimeout: 'readonly',
                setInterval: 'readonly',
                clearTimeout: 'readonly',
                clearInterval: 'readonly',
                // GNOME Shell Only
                global: 'readonly',
                _: 'readonly',
                C_: 'readonly',
                N_: 'readonly',
                ngettext: 'readonly',
            },
            parserOptions: {
                ecmaVersion: 2022,
                sourceType: 'module',
            },
        },
        rules: {
            // See: https://eslint.org/docs/latest/rules/#possible-problems
            'array-callback-return': 'error',
            'no-await-in-loop': 'error',
            'no-constant-binary-expression': 'error',
            'no-constructor-return': 'error',
            'no-new-native-nonconstructor': 'error',
            'no-promise-executor-return': 'error',
            'no-self-compare': 'error',
            'no-template-curly-in-string': 'error',
            'no-unmodified-loop-condition': 'error',
            'no-unreachable-loop': 'error',
            'no-unused-private-class-members': 'error',
            'no-use-before-define': [
                'error',
                {
                    functions: false,
                    classes: true,
                    variables: true,
                    allowNamedExports: true,
                },
            ],
            // See: https://eslint.org/docs/latest/rules/#suggestions
            'block-scoped-var': 'error',
            'complexity': 'warn',
            'consistent-return': 'error',
            'default-param-last': 'error',
            'eqeqeq': 'error',
            'no-array-constructor': 'error',
            'no-caller': 'error',
            'no-extend-native': 'error',
            'no-extra-bind': 'error',
            'no-extra-label': 'error',
            'no-iterator': 'error',
            'no-label-var': 'error',
            'no-loop-func': 'error',
            'no-multi-assign': 'warn',
            'no-new-object': 'error',
            'no-new-wrappers': 'error',
            'no-proto': 'error',
            'no-shadow': 'warn',
            'no-unused-vars': [
                'error',
                {
                    varsIgnorePattern: '^_',
                    argsIgnorePattern: '^_',
                },
            ],
            'no-var': 'warn',
            'unicode-bom': 'error',
            // GJS Restrictions
            'no-restricted-globals': [
                'error',
                {
                    name: 'Debugger',
                    message: 'Internal use only',
                },
                {
                    name: 'GIRepositoryGType',
                    message: 'Internal use only',
                },
                {
                    name: 'log',
                    message: 'Use console.log()',
                },
                {
                    name: 'logError',
                    message: 'Use console.warn() or console.error()',
                },
            ],
            'no-restricted-properties': [
                'error',
                {
                    object: 'imports',
                    property: 'format',
                    message: 'Use template strings',
                },
                {
                    object: 'pkg',
                    property: 'initFormat',
                    message: 'Use template strings',
                },
                {
                    object: 'Lang',
                    property: 'copyProperties',
                    message: 'Use Object.assign()',
                },
                {
                    object: 'Lang',
                    property: 'bind',
                    message: 'Use arrow notation or Function.prototype.bind()',
                },
                {
                    object: 'Lang',
                    property: 'Class',
                    message: 'Use ES6 classes',
                },
            ],
            'no-restricted-syntax': [
                'error',
                {
                    selector: 'MethodDefinition[key.name="_init"] CallExpression[arguments.length<=1][callee.object.type="Super"][callee.property.name="_init"]',
                    message: 'Use constructor() and super()',
                },
            ],
        },
    },
];

Continuous Integration

In most projects, it is recommended practice to run tests on every pull request before merging into the main branch. Below are two example CI configurations for running ESLint with GitLab and GitHub.

GitLab (.gitlab-ci.yml)
yml
image: node:latest

stages:
- lint

eslint:
  stage: lint
  script:
    - export NODE_PATH=$(npm root -g)
    - npm install -g eslint@^8.0.0
    - eslint --format junit --output-file eslint-report.xml .
  artifacts:
    reports:
      junit: eslint-report.xml
    when: always
  rules:
    - when: always
GitHub (.github/workflows/eslint.yml)
yml
name: ESLint

on:
  push:
    branches: [ 'main' ]
  pull_request:
    branches: [ 'main' ]
  schedule:
    - cron: '33 14 * * 5'

jobs:
  eslint:
    name: Run eslint scanning
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
      # Required for private repositories by github/codeql-action/upload-sarif
      actions: read
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Install
        run: |
          npm install eslint@^8.0.0
          npm install @microsoft/eslint-formatter-sarif@2.1.7

      - name: Lint
        run: npx eslint .
          --format @microsoft/eslint-formatter-sarif
          --output-file eslint-results.sarif
        continue-on-error: true

      - name: Report
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: eslint-results.sarif
          wait-for-processing: true

Prettier

Prettier is another popular tool for JavaScript projects, renowned for its lack of options. It focuses specifically on formatting code, and won't catch logic errors or anti-patterns like ESLint.

Below is a sample configuration (with almost all available options), set to resemble the code style used by many GJS applications:

yml
tabWidth: 4
useTabs: false
semi: true
singleQuote: true
quoteProps: 'as-needed'
trailingComma: 'es5'
bracketSpacing: false
arrowParens: 'avoid'

EditorConfig

EditorConfig is a more general formatting tool, targeted directly at IDEs like GNOME Builder and VSCode. It's used to tell an editor to trim trailing whitespace, what indentation to use, and other similar preferences.

Below is the .editorconfig file used in the GJS project:

ini
# SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
# SPDX-FileCopyrightText: 2021 Sonny Piers <sonny@fastmail.net>

# EditorConfig is awesome: https://EditorConfig.org

root = true

[*]
indent_style = space
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
end_of_line = lf
insert_final_newline = true

[*.js]
quote_type = single

Code Conventions

The following guidelines are general recommendations and coding conventions followed by many GJS projects. As general rule, you should take advantage of modern language features, both in JavaScript and GJS.

Files and Imports

TIP

GJS has supported ESModules since GNOME 40, and GNOME Shell extensions are required to use them since GNOME 45.

JavaScript file names should be lowerCamelCase with a .js extension, while directories should be short and lowercase:

sh
js/misc/extensionSystem.js
js/ui/panel.js

Use PascalCase when importing modules and classes

js
import * as Util from 'resource:///gjs/guide/Example/js/util.js';

Keep library, module and local imports separated by a single line.

js
import Gio from 'gi://Gio';

import * as Main from 'resource:///org/gnome/shell/ui/main.js';

import * as Util from './lib/util.js';

GObject

TIP

See GObject Basics for more details about using GObject in JavaScript.

Properties

When possible, set all properties when constructing an object, which is cleaner and avoids extra property notifications.

js
const label = new Gtk.Label({
    label: 'Example',
});

Using camelCase property accessors is preferred by many GNOME projects. GJS can automatically convert GObject property names, except when used as a string.

js
label.useMarkup = true;

label.bind_property('use-markup', label, 'use-underline',
    GObject.BindFlags.SYNC_CREATE | GObject.BindFlags.INVERT_BOOLEAN);

Asynchronous Operations

Use Gio._promisify() to enable async/await with asynchronous methods in platform libraries:

js
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';

Gio._promisify(Gio.File.prototype, 'delete_async');

const file = Gio.File.new_for_path('file.txt');
await file.delete_async(GLib.PRIORITY_DEFAULT, null /* cancellable */);

JavaScript

Variables and Exports

Use const when the value will be bound to a static value, and let when you need a mutable variable:

js
const elementCount = 10;
const elements = [];

for (let i = 0; i < elementCount; i++)
    elements.push(i);

for (const element of elements)
    console.log(`Element #${element + 1}`);

The var statement should be avoided, since it has unexpected behavior like hoisting. Although it was used in older code to make members of a script public, export should now be used in all new code:

js
export const PUBLIC_CONSTANT = 100;

export const PublicObject = GObject.registerClass(
class PublicObject extends GObject.Object {
    frobnicate() {
    }
});

Classes and Functions

Define classes with class and override the standard constructor() when subclassing GObject classes:

js
class MyObject {
    frobnicate() {
    }
}

const MySubclass = GObject.registerClass(
class MySubclass extends GObject.Object {
    constructor(params = {}) {
        /* Chain-up with an object of construct properties */
        super(params);
    }

    frobnicate() {
    }
});

Use arrow functions for inline callbacks and Function.prototype.bind() for larger functions.

js
class MyClock {
    constructor() {
        this._settings = new Gio.Settings({
            schema_id: 'org.gnome.desktop.interface',
        });

        this._settings.connect('changed::clock-show-seconds', () => {
            this.showSeconds = this._settings.get_boolean('clock-show-seconds');
        });

        this._settings.connect('changed::clock-show-weekdays',
            this._onShowWeekdaysChanged.bind(this));
    }

    _onShowWeekdaysChanged() {
        this.showWeekdays = this._settings.get_boolean('clock-show-weekdays');
    }
}

MIT Licensed | GJS, A GNOME Project