Creating the Project

This section contains a lot a boilerplate code, but that is necessary in order to have an easy to work with environment!

# Choosing an Application ID

The application ID will identify our application across the platform. It is in the reverse DNS form, and should be composed of a domain we own, and the application name.

Let’s say we control the domain, and our application name is “File Browser”. The application ID is then org.example.filebrowser.

# Directory Structure

It’s time to create a source directory for our application. We name it by the ID of our application:

1mkdir org.example.filebrowser

Inside, we create the directory structure that will contain our project files:

  • src will contain our JavaScript modules
  • data will contain our additional application data, such as icons, UI definitions, CSS files

You can use these commands to create these directories in your application directory:

1cd org.example.filebrowser
2mkdir src data

# Necessary Files

Now let’s create all the basic files required for running our application.

# Main Module

This file is the heart of our application. It must contain a main() function that will be run when it starts. Let’s create it in src/main.js, and print a simple message to the console inside:

1export function main(argv) {
2	console.log('Hello World!');

# Resources

The build system will compile our sources and data files into GResources. We have to create two XML files describing our resources.


1<?xml version="1.0" encoding="UTF-8"?>
3	<gresource prefix="/org/example/filebrowser/js">
4		<file>main.js</file>
5	</gresource>


1<?xml version="1.0" encoding="UTF-8"?>
3	<gresource prefix="/org/example/filebrowser">
4	</gresource>

Don’t forget to update these files when we add files to the application!

# Entry Point

This is a special file that will initialize our GJS package and start the application. Create a src/org.example.filebrowser.js file and put this inside:

 1#!@GJS@ -m
 3import GLib from 'gi://GLib';
 5// Initialize the package
 7	name: '@PACKAGE_NAME@',
 8	version: '@PACKAGE_VERSION@',
 9	prefix: '@PREFIX@',
10	libdir: '@LIBDIR@',
13// Import the main module and run the main function
14const loop = new GLib.MainLoop(null, false);
16	.then((main) => {
17		GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
18			loop.quit();
20			return GLib.SOURCE_REMOVE;
21		});
22	})
23	.catch(logError);;

All the values between @ will be replaced by the build system when compiling the application.

So what does it do? package is a special GJS module containing functions that will initialize our package (package.init()) by setting some directories and loading resources, and run our application (

After initializing the package, we create a loop to asynchronously load our main module from the resources. The loop is then stopped, and our application takes over from there.

# Build System

Now, we have to tell Meson, our build system, what to do with all these files. Let’s create all the files with our instructions.

In, we give some information about our project, set the APP_ID variable to reuse the application ID elsewhere without having to retype it, import the gnome module, and load files from the data and src directories:

 1# Define our project
 3	'filebrowser',
 4	version: '0.0.1',
 5	license: ['GPL-3.0-or-later'],
 6	meson_version: '>= 0.59.0'
 9APP_ID = 'org.example.filebrowser'
11# Import the modules
12gnome = import('gnome')
14# Load instructions from subdirectories

In data/, we compile the data resources with the gnome.compile_resources() function and install them in our application data directory:

1# Compile the resources
3	APP_ID + '.data',
4	APP_ID + '.data.gresource.xml',
5	gresource_bundle: true,
6	install: true,
7	install_dir: get_option('datadir') / APP_ID

In src/, we also compile the source resources, and we configure our entry point file and install it as an executable in the binaries directory:

 1# Configure the entry point
 3	input : APP_ID + '.js',
 4	output : APP_ID,
 5	configuration: {
 6		'GJS': find_program('gjs').full_path(),
 8		'PACKAGE_VERSION': meson.project_version(),
 9		'PREFIX': get_option('prefix'),
10		'LIBDIR': get_option('prefix') / get_option('libdir')
11	},
12	install_dir: get_option('bindir'),
13	install_mode: 'rwxr-xr-x'
16# Compile the resources
17app_resource = gnome.compile_resources(
18	APP_ID + '.src',
19	APP_ID + '.src.gresource.xml',
20	gresource_bundle: true,
21	install: true,
22	install_dir : get_option('datadir') / APP_ID

# Flatpak

Finally, let’s write the Flatpak manifest that will allow us to build and run the app in a well-known environment. Put this in the org.example.filebrowser.yml file:

 1app-id: org.example.filebrowser
 2runtime: org.gnome.Platform
 3runtime-version: '43'
 4sdk: org.gnome.Sdk
 5command: org.example.filebrowser
 8  - --socket=wayland
 9  - --socket=fallback-x11
10  - --share=ipc
11  - --device=dri
14  - /include
15  - /lib/pkgconfig
16  - /share/doc
17  - /share/man
18  - '*.a'
19  - '*.la'
23  - name: filebrowser
24    buildsystem: meson
25    sources:
26      - type: dir
27        path: .

# Building and Running Our Application

It’s now time to test that our app is working! First install the necessary Flatpak runtime and SDK:

1flatpak install --user org.gnome.Platform//43 org.gnome.Sdk//43

Then install the Flatpak Builder tool:

1flatpak install org.flatpak.Builder

Now we can build the application:

1flatpak run org.flatpak.Builder --force-clean --user --install build-dir org.example.filebrowser.yml

And run it:

1flatpak run org.example.filebrowser

If you did everything correctly, Hello World! should be printed in the console.

All this work for that? It’s now time to do some more interesting things!