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 globalopen in new window.

ESLintopen in new window 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 configurationopen in new window 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.

Recommended Configuration (.eslintrc.yml)
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
parserOptions:
  ecmaVersion: 2022
  sourceType: module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106

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)
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GitHub (.github/workflows/eslint.yml)
name: ESLint

on:
  push:
    branches: [ 'main' ]
  pull_request:
    # The branches below must be a subset of the branches above
    branches: [ 'main' ]
  schedule:
    - cron: '33 14 * * 5'

jobs:
  eslint:
    name: Run eslint scanning
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
      actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
    steps:
      - name: Checkout code
        uses: actions/checkout@v3

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

      - name: Run ESLint
        run: npx eslint .
          --ext .js,.jsx,.ts,.tsx
          --format @microsoft/eslint-formatter-sarif
          --output-file eslint-results.sarif
        continue-on-error: true

      - name: Upload analysis results to GitHub
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: eslint-results.sarif
          wait-for-processing: true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

Prettier

Prettieropen in new window 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:

tabWidth: 4
useTabs: false
semi: true
singleQuote: true
quoteProps: 'as-needed'
trailingComma: 'es5'
bracketSpacing: false
arrowParens: 'avoid'
1
2
3
4
5
6
7
8

EditorConfig

EditorConfigopen in new window 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:

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
1
2
3
4
5
6
7
8
9
10
11
12

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:

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

Use CamelCase when importing modules and classes

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

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

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.

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.

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:

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:

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 hoistingopen in new window. Although it was used in older code to make members of a script public, export should now be used in all new code:

export const PUBLIC_CONSTANT = 100;

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

Classes and Functions

Define classes with classopen in new window and override the standard constructor() when subclassing GObject classes:

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 functionsopen in new window for inline callbacks and Function.prototype.bind()open in new window for larger functions.

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');
    }
}
Last Updated: 9/23/2023, 4:00:18 AM
Contributors: Ryann Ferreira, Andy Holmes, Andy Holmes, Evan Welsh, Evan Welsh