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
# 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
// 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
)
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
)
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:
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:
# 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
:
js/misc/extensionSystem.js
js/ui/panel.js
Use PascalCase
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 hoisting. 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 class
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 functions for inline callbacks and Function.prototype.bind()
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');
}
}