Add guides on Getting Started with JS (#4150)

- Get started section added to the JS docs
- Small fixes in the JS modules docs
This commit is contained in:
Ruslan Nadyrshin
2025-03-18 20:08:23 +04:00
committed by GitHub
parent 9a8dcc340f
commit 02dedd60f3
25 changed files with 343 additions and 123 deletions

View File

@@ -0,0 +1,16 @@
# About the JavaScript engine {#js_about_js_engine}
> Developing applications for Flipper Zero is now much more accessible with the introduction of JavaScript support.
Previously, building an app for Flipper Zero required C/C++ skills, setting up a development environment, and studying the code of existing applications and documentation. While embedded developers are very familiar with all of this, we wanted to make it easier for people from all backgrounds to create apps for Flipper Zero.
Flipper firmware now includes a built-in scripting engine that runs JavaScript, one of the most widely used programming languages. You can create script files, share them with others, and launch them directly from the **Apps/Scripts** menu on your Flipper Zero — no need for compiling on a PC.
JavaScript support is based on the [mJS scripting engine](https://github.com/cesanta/mjs). Originally designed for microcontrollers, mJS makes efficient use of system resources, requiring less than 50k of flash space and 2k of RAM. We've kept the core features of mJS and also added some useful improvements, such as support for compact binary arrays.
> [!note]
> mJS has some limitations compared to JavaScript engines built into modern browsers. For details on capabilities and limitations, refer to the [mJS documentation on GitHub](https://github.com/cesanta/mjs).
JavaScript apps can interact with Flipper Zero's resources, including its GUI, buttons, USB-HID device, GPIO, UART interfaces, and more. Let's go through the steps to create your first JavaScript app for Flipper Zero.
**Next step:** [Your first JavaScript app](#js_your_first_js_app)

View File

@@ -1,6 +1,5 @@
# js_badusb {#js_badusb}
# BadUSB module {#js_badusb}
# BadUSB module
```js
let badusb = require("badusb");
```
@@ -58,7 +57,7 @@ badusb.press(0x47); // Press key with HID code (hex) 0x47 (Scroll lock)
Hold a key. Up to 5 keys (excluding modifiers) can be held simultaneously.
### Parameters
Same as `press`
Same as `press`.
### Examples:
```js
@@ -70,9 +69,9 @@ badusb.hold("CTRL", "v"); // Press and hold CTRL + "v" combo
Release a previously held key.
### Parameters
Same as `press`
Same as `press`.
Release all keys if called without parameters
Release all keys if called without parameters.
### Examples:
```js
@@ -85,7 +84,7 @@ Print a string.
### Parameters
- A string to print
- (optional) delay between key presses
- *(optional)* Delay between key presses
### Examples:
```js
@@ -98,7 +97,7 @@ Same as `print` but ended with "ENTER" press.
### Parameters
- A string to print
- (optional) delay between key presses
- *(optional)* Delay between key presses
### Examples:
```js

View File

@@ -0,0 +1,77 @@
# Developing apps using JavaScript SDK {#js_developing_apps_using_js_sdk}
In the [previous guide](#js_your_first_js_app), we learned how to create and run a JavaScript app on Flipper Zero. However, when debugging a script, you often need to repeatedly modify the code and test it on the device. While you can use qFlipper for this, it involves a lot of repetitive steps. Fortunately, there's a more efficient alternative — the Flipper Zero JavaScript SDK, a set of tools that simplify app development in JavaScript.
Main features of the Flipper Zero JavaScript SDK:
* [Loading and running an app with a single command](#js_sdk_run_app)
* [Code completion](#js_sdk_code_completion)
* [JS code minifier (compressor)](#js_sdk_js_minifier)
In this guide, we'll install the JavaScript SDK and learn how to run JavaScript apps on Flipper Zero using it.
## How to get JavaScript SDK
The JavaScript SDK for Flipper Zero is distributed as an [NPM package](npmjs.com/package/\@flipperdevices/fz-sdk), so you can install it using a package manager like npm, pnpm, or yarn. You'll also need Node.js, a JavaScript runtime environment required for the NPM package manager to work.
> [!note]
> In this guide, we'll use **npm**, the default package manager for Node.js.
Follow these steps:
1. Install **Node.js + npm** on your PC. Check out this [official Downloads page](https://nodejs.org/en/download/package-manager), select your OS and preferences, and run the provided commands in your terminal.
2. Open a terminal in the folder where you want to store your project.
3. Run the `npx @flipperdevices/create-fz-app@latest` command to create a JavaScript app template and include the JavaScript SDK into it. This command will launch an interactive wizard. You'll need to specify the project name and choose a package manager (in our case, **npm**).
You'll now find a JavaScript app template in your project folder, alongside the JavaScript SDK package, all necessary dependencies and configs. The app code will be in the `index.ts` file.
Now, let's take a look at the main features of the Flipper Zero JavaScript SDK.
## Running your app {#js_sdk_run_app}
To run the application:
1. Connect your Flipper Zero to your PC via USB.
2. Open a terminal in your app's folder.
3. Run the `npm start` command to copy the JS file to Flipper Zero and run it.
\image html js_sdk_npm_start.jpg width=800
You'll see output messages from the `print()` function in the terminal.
## Updating your app {#js_sdk_update_app}
After making changes to your app's code, simply run `npm start` again. As long as your Flipper Zero is still connected, the updated app will launch, and the old `.js` file on Flipper Zero will be replaced with the new version.
## Other JavaScript SDK features
As you can see, it's quite easy to launch and update your app with a single command. Now let's explore two more important features of the Flipper Zero JavaScript SDK: **code completion** and **JS minifier**.
### Code completion {#js_sdk_code_completion}
Code completion helps speed up the development process by automatically suggesting code as you type, reducing the need to refer to documentation.
\image html js_sdk_code_completion.jpg width=800
> [!note]
> Code completion works in code editors and IDEs that support Language Server, for example, [VS Code](https://code.visualstudio.com/).
### JS minifier {#js_sdk_js_minifier}
The JS minifier reduces the size of JavaScript files by removing unnecessary characters (like spaces, tabs and line breaks) and shortening variable names. This can make your scripts run a bit faster without changing their logic.
However, it has a drawback — it can make debugging harder, as error messages in minified files are harder to read in larger applications. For this reason, it's recommended to disable the JS minifier during debugging and it's disabled by default. To enable it, set the `minify` parameter to `true` in the `fz-sdk.config.json5` file in your app folder. This will minify your JavaScript app before loading it onto Flipper Zero.
## What's next?
You've learned how to run and debug simple JavaScript apps. But how can you access Flipper Zero's hardware from your JS code? For that, you'll need to use JS modules — which we'll cover in the next guide.
**Next step:** [Using JavaScript modules](#js_using_js_modules)

View File

@@ -1,6 +1,5 @@
# js_event_loop {#js_event_loop}
# Event Loop module {#js_event_loop}
# Event Loop module
```js
let eventLoop = require("event_loop");
```
@@ -84,7 +83,7 @@ Because we have two extra arguments, if we return anything other than an array
of length 2, the arguments will be kept as-is for the next call.
The first two arguments that get passed to our callback are:
- The subscription manager that lets us `.cancel()` our subscription
- The subscription manager that lets us `.cancel()` our subscription.
- The event item, used for events that have extra data. Timer events do not,
they just produce `undefined`.

View File

@@ -1,14 +1,12 @@
# js_gpio {#js_gpio}
# GPIO module {#js_gpio}
The module allows you to control GPIO pins of the expansion connector on Flipper Zero. Call the `require` function to load the module before first using its methods. This module depends on the `event_loop` module, so it **must** be imported after `event_loop` is imported:
# GPIO module
```js
let eventLoop = require("event_loop");
let gpio = require("gpio");
```
This module depends on the `event_loop` module, so it _must_ only be imported
after `event_loop` is imported.
# Example
```js
let eventLoop = require("event_loop");
@@ -31,11 +29,11 @@ Gets a `Pin` object that can be used to manage a pin.
- `pin`: pin identifier (examples: `"pc3"`, `7`, `"pa6"`, `3`)
### Returns
A `Pin` object
A `Pin` object.
## `Pin` object
### `Pin.init()`
Configures a pin
Configures a pin.
#### Parameters
- `mode`: `Mode` object:
@@ -49,28 +47,28 @@ Configures a pin
- `pull` (optional): either `"up"`, `"down"` or unset
### `Pin.write()`
Writes a digital value to a pin configured with `direction: "out"`
Writes a digital value to a pin configured with `direction: "out"`.
#### Parameters
- `value`: boolean logic level to write
### `Pin.read()`
Reads a digital value from a pin configured with `direction: "in"` and any
`inMode` except `"analog"`
`inMode` except `"analog"`.
#### Returns
Boolean logic level
### `Pin.readAnalog()`
Reads an analog voltage level in millivolts from a pin configured with
`direction: "in"` and `inMode: "analog"`
`direction: "in"` and `inMode: "analog"`.
#### Returns
Voltage on pin in millivolts
Voltage on pin in millivolts.
### `Pin.interrupt()`
Attaches an interrupt to a pin configured with `direction: "in"` and
`inMode: "interrupt"` or `"event"`
`inMode: "interrupt"` or `"event"`.
#### Returns
An event loop `Contract` object that identifies the interrupt event source. The

View File

@@ -1,13 +1,22 @@
# js_gui {#js_gui}
# GUI module {#js_gui}
The module allows you to use GUI (graphical user interface) in concepts off the Flipper Zero firmware. Call the `require` function to load the module before first using its methods. This module depends on the `event_loop` module, so it **must** be imported after the `event_loop` import:
# GUI module
```js
let eventLoop = require("event_loop");
let gui = require("gui");
```
## Submodules
This module depends on the `event_loop` module, so it _must_ only be imported
after `event_loop` is imported.
GUI module has several submodules:
- @subpage js_gui__submenu — Displays a scrollable list of clickable textual entries
- @subpage js_gui__loading — Displays an animated hourglass icon
- @subpage js_gui__empty_screen — Just empty screen
- @subpage js_gui__text_input — Keyboard-like text input
- @subpage js_gui__text_box — Simple multiline text box
- @subpage js_gui__dialog — Dialog with up to 3 options
- @subpage js_gui__widget — Displays a combination of custom elements on one screen
## Conceptualizing GUI
### Event loop
@@ -27,23 +36,23 @@ always access the canvas through a viewport.
In Flipper's terminology, a "View" is a fullscreen design element that assumes
control over the entire viewport and all input events. Different types of views
are available (not all of which are unfortunately currently implemented in JS):
| View | Has JS adapter? |
|----------------------|-----------------------|
| `button_menu` | ❌ |
| `button_panel` | ❌ |
| `byte_input` | |
| `dialog_ex` | ✅ (as `dialog`) |
| `empty_screen` | ✅ |
| `file_browser` | ✅ (as `file_picker`) |
| `loading` | ✅ |
| `menu` | ❌ |
| `number_input` | ❌ |
| `popup` | ❌ |
| `submenu` | ✅ |
| `text_box` | ✅ |
| `text_input` | ✅ |
| `variable_item_list` | ❌ |
| `widget` | |
| View | Has JS adapter? |
|----------------------|------------------|
| `button_menu` | ❌ |
| `button_panel` | ❌ |
| `byte_input` | |
| `dialog_ex` | ✅ (as `dialog`) |
| `empty_screen` | ✅ |
| `file_browser` | |
| `loading` | ✅ |
| `menu` | ❌ |
| `number_input` | ❌ |
| `popup` | ❌ |
| `submenu` | ✅ |
| `text_box` | ✅ |
| `text_input` | ✅ |
| `variable_item_list` | ❌ |
| `widget` | |
In JS, each view has its own set of properties (or just "props"). The programmer
can manipulate these properties in two ways:
@@ -119,7 +128,7 @@ eventLoop.run();
The `viewDispatcher` constant holds the `ViewDispatcher` singleton.
### `viewDispatcher.switchTo(view)`
Switches to a view, giving it control over the display and input
Switches to a view, giving it control over the display and input.
#### Parameters
- `view`: the `View` to switch to
@@ -127,24 +136,24 @@ Switches to a view, giving it control over the display and input
### `viewDispatcher.sendTo(direction)`
Sends the viewport that the dispatcher manages to the front of the stackup
(effectively making it visible), or to the back (effectively making it
invisible)
invisible).
#### Parameters
- `direction`: either `"front"` or `"back"`
### `viewDispatcher.sendCustom(event)`
Sends a custom number to the `custom` event handler
Sends a custom number to the `custom` event handler.
#### Parameters
- `event`: number to send
### `viewDispatcher.custom`
An event loop `Contract` object that identifies the custom event source,
triggered by `ViewDispatcher.sendCustom(event)`
triggered by `ViewDispatcher.sendCustom(event)`.
### `viewDispatcher.navigation`
An event loop `Contract` object that identifies the navigation event source,
triggered when the back key is pressed
triggered when the back key is pressed.
## `ViewFactory`
When you import a module implementing a view, a `ViewFactory` is instantiated.
@@ -152,10 +161,10 @@ For example, in the example above, `loadingView`, `submenuView` and `emptyView`
are view factories.
### `ViewFactory.make()`
Creates an instance of a `View`
Creates an instance of a `View`.
### `ViewFactory.make(props)`
Creates an instance of a `View` and assigns initial properties from `props`
Creates an instance of a `View` and assigns initial properties from `props`.
#### Parameters
- `props`: simple key-value object, e.g. `{ header: "Header" }`

View File

@@ -1,6 +1,5 @@
# js_gui__dialog {#js_gui__dialog}
# Dialog GUI view {#js_gui__dialog}
# Dialog GUI view
Displays a dialog with up to three options.
<img src="dialog.png" width="200" alt="Sample screenshot of the view" />
@@ -16,16 +15,16 @@ This module depends on the `gui` module, which in turn depends on the
recommended to conceptualize these modules first before using this one.
# Example
For an example refer to the `gui.js` example script.
For an example, refer to the `gui.js` example script.
# View props
## `header`
Text that appears in bold at the top of the screen
Text that appears in bold at the top of the screen.
Type: `string`
## `text`
Text that appears in the middle of the screen
Text that appears in the middle of the screen.
Type: `string`

View File

@@ -1,7 +1,6 @@
# js_gui__empty_screen {#js_gui__empty_screen}
# Empty Screen GUI view {#js_gui__empty_screen}
# Empty Screen GUI View
Displays nothing.
Displays an empty screen.
<img src="empty.png" width="200" alt="Sample screenshot of the view" />
@@ -16,7 +15,7 @@ This module depends on the `gui` module, which in turn depends on the
recommended to conceptualize these modules first before using this one.
# Example
For an example refer to the GUI example.
For an example, refer to the GUI example.
# View props
This view does not have any props.

View File

@@ -1,8 +1,6 @@
# js_gui__loading {#js_gui__loading}
# Loading GUI view {#js_gui__loading}
# Loading GUI View
Displays an animated hourglass icon. Suppresses all `navigation` events, making
it impossible for the user to exit the view by pressing the back key.
Displays an animated hourglass icon. Suppresses all `navigation` events, making it impossible for the user to exit the view by pressing the BACK key.
<img src="loading.png" width="200" alt="Sample screenshot of the view" />

View File

@@ -1,6 +1,5 @@
# js_gui__submenu {#js_gui__submenu}
# Submenu GUI view {#js_gui__submenu}
# Submenu GUI view
Displays a scrollable list of clickable textual entries.
<img src="submenu.png" width="200" alt="Sample screenshot of the view" />
@@ -16,16 +15,16 @@ This module depends on the `gui` module, which in turn depends on the
recommended to conceptualize these modules first before using this one.
# Example
For an example refer to the GUI example.
For an example, refer to the GUI example.
# View props
## `header`
Single line of text that appears above the list
A single line of text that appears above the list.
Type: `string`
## `items`
The list of options
The list of options.
Type: `string[]`

View File

@@ -1,6 +1,5 @@
# js_gui__text_box {#js_gui__text_box}
# Text box GUI view {#js_gui__text_box}
# Text box GUI view
Displays a scrollable read-only text field.
<img src="text_box.png" width="200" alt="Sample screenshot of the view" />
@@ -16,7 +15,7 @@ This module depends on the `gui` module, which in turn depends on the
recommended to conceptualize these modules first before using this one.
# Example
For an example refer to the `gui.js` example script.
For an example, refer to the `gui.js` example script.
# View props
## `text`

View File

@@ -1,6 +1,5 @@
# js_gui__text_input {#js_gui__text_input}
# Text input GUI view {#js_gui__text_input}
# Text input GUI view
Displays a keyboard.
<img src="text_input.png" width="200" alt="Sample screenshot of the view" />
@@ -16,29 +15,29 @@ This module depends on the `gui` module, which in turn depends on the
recommended to conceptualize these modules first before using this one.
# Example
For an example refer to the `gui.js` example script.
For an example, refer to the `gui.js` example script.
# View props
## `minLength`
Smallest allowed text length
The shortest allowed text length.
Type: `number`
## `maxLength`
Biggest allowed text length
The longest allowed text length.
Type: `number`
Default: `32`
## `header`
Single line of text that appears above the keyboard
A single line of text that appears above the keyboard.
Type: `string`
# View events
## `input`
Fires when the user selects the "save" button and the text matches the length
Fires when the user selects the "Save" button and the text matches the length
constrained by `minLength` and `maxLength`.
Item type: `string`

View File

@@ -1,7 +1,6 @@
# js_gui__widget {#js_gui__widget}
# Widget GUI view {#js_gui__widget}
# Widget GUI view
Displays a combination of custom elements on one screen
Displays a combination of custom elements on one screen.
<img src="widget.png" width="200" alt="Sample screenshot of the view" />
@@ -16,7 +15,7 @@ This module depends on the `gui` module, which in turn depends on the
recommended to conceptualize these modules first before using this one.
# Example
For an example refer to the `gui.js` example script.
For an example, refer to the `gui.js` example script.
# View props
This view does not have any props.

View File

@@ -1,6 +1,7 @@
# js_math {#js_math}
# Math module {#js_math}
The module contains mathematical methods and constants. Call the `require` function to load the module before first using its methods:
# Math module
```js
let math = require("math");
```

View File

@@ -1,13 +1,12 @@
# js_notification {#js_notification}
# Notification module {#js_notification}
# Notification module
```js
let notify = require("notification");
```
# Methods
## success
"Success" flipper notification message
"Success" flipper notification message.
### Examples:
```js
@@ -15,7 +14,7 @@ notify.success();
```
## error
"Error" flipper notification message
"Error" flipper notification message.
### Examples:
```js
@@ -23,7 +22,7 @@ notify.error();
```
## blink
Blink notification LED
Blink notification LED.
### Parameters
- Blink color (blue/red/green/yellow/cyan/magenta)

View File

@@ -1,6 +1,5 @@
# js_serial {#js_serial}
# Serial module {#js_serial}
# Serial module
```js
let serial = require("serial");
```
@@ -20,7 +19,7 @@ serial.setup("lpuart", 115200);
```
## write
Write data to serial port
Write data to serial port.
### Parameters
One or more arguments of the following types:
@@ -41,7 +40,7 @@ Read a fixed number of characters from serial port.
### Parameters
- Number of bytes to read
- (optional) Timeout value in ms
- *(optional)* Timeout value in ms
### Returns
A sting of received characters or undefined if nothing was received before timeout.
@@ -53,10 +52,10 @@ serial.read(10, 5000); // Read 10 bytes, with 5s timeout
```
## readln
Read from serial port until line break character
Read from serial port until line break character.
### Parameters
(optional) Timeout value in ms
*(optional)* Timeout value in ms.
### Returns
A sting of received characters or undefined if nothing was received before timeout.
@@ -68,11 +67,11 @@ serial.readln(5000); // Read with 5s timeout
```
## readBytes
Read from serial port until line break character
Read from serial port until line break character.
### Parameters
- Number of bytes to read
- (optional) Timeout value in ms
- *(optional)* Timeout value in ms
### Returns
ArrayBuffer with received data or undefined if nothing was received before timeout.
@@ -86,13 +85,13 @@ serial.readBytes(1, 0);
```
## expect
Search for a string pattern in received data stream
Search for a string pattern in received data stream.
### Parameters
- Single argument or array of the following types:
- A string
- Array of numbers, each number is interpreted as a byte
- (optional) Timeout value in ms
- *(optional)* Timeout value in ms
### Returns
Index of matched pattern in input patterns list, undefined if nothing was found.

View File

@@ -0,0 +1,42 @@
# Using JavaScript modules {#js_using_js_modules}
In the previous guides, we learned how to write a basic JavaScript app using [built-in functions](#js_builtin). However, the set of built-in functions is limited, so when developing your JS apps, you'll likely want to use external JS modules. These modules offer a wide range of functions (methods) for various tasks.
For example:
* The `serial` module enables transmitting and receiving data via a serial interface
* The `badusb` module enables USB keyboard emulation and sending key press events via USB
* The `math` module provides mathematical functions
JS modules are written in C/C++, making them fast and efficient. They come with Flipper Zero firmware and are stored on the microSD card in compiled form as **FAL (Flipper Application File)** files.
> [!note]
> You can find the implementation of all supported JS modules in the [Flipper Zero firmware repository](https://github.com/flipperdevices/flipperzero-firmware/tree/dev/applications/system/js_app/modules). Also, check out the [docs for JS modules](#js_modules) for more details.
## How to use JS modules in your app
Before using any of the JS module methods, you **must** import the module using the `require()` function. This loads the module into RAM, allowing you to access its methods.
To save RAM and improve performance, avoid loading modules you don't plan to use. Also, all loaded modules will be automatically unloaded from RAM after the app execution ends.
To load a module, call the `require()` function with the module name in quotes. For example, to load the `notification` module, write this:
\code{.js}
let notify = require("notification");
\endcode
Now you can call methods of the `notification` module using the `notify` variable to access them:
\code{.js}
let notify = require("notification");
notify.success();
print("success notification");
\endcode
## What's next?
Congratulations, you've completed the **Getting Started** section of our JS docs. You've learned how to run and debug JS apps, and how to use JS modules. Now, we invite you to check out the [main JavaScript page](#js) where you'll find:
* JavaScript app examples
* Documentation on JS modules
* Additional resources related to JavaScript on Flipper Zero

View File

@@ -0,0 +1,83 @@
# Your first JavaScript app {#js_your_first_js_app}
In this guide, we'll create a simple script that outputs ordinal numbers with a delay and learn how to run it on Flipper Zero in different ways. All you need is your Flipper Zero, a PC, and a USB cable.
## Step 1. Create the script file
Create a new text file `first_app.js`. Paste the code below into it and save the file:
\code{.js}
print("start");
delay(1000);
print("1");
delay(500);
print("2");
delay(500);
print("3");
delay(500);
print("end");
\endcode
What the code does:
* Outputs the text **start**, waits 1 second
* Outputs the numbers **1**, **2** and **3**, with a 0.5-second pause after each number
* Outputs the text **end**
The `print()` function is used to output text. The string to be output is in the brackets. This is a built-in function, so you don't need to include additional JS modules. You can use this function anywhere in your application.
Another built-in function, `delay()`, implements delay. The delay time in milliseconds is given in the brackets. Since 1000 milliseconds equals 1 second, a 1-second delay is written as 1000, and a 0.5-second delay as 500.
> [!note]
> Find the list of built-in functions in [Built-in functions](#js_builtin).
## Step 2. Copy the file to Flipper Zero
To copy the JavaScript file to Flipper Zero, follow these steps:
1. Connect your Flipper Zero to your PC via USB.
2. Open the **qFlipper** application.
3. Go to the **File manager** tab and open the path `SD Card/apps/Scripts/`.
4. Drag and drop the file into the qFlipper window.
> [!note]
> To learn more about qFlipper, visit the [dedicated section in our user documentation](https://docs.flipper.net/qflipper).
Your script is now ready to run on Flipper Zero.
## Step 3. Run your script
You can launch your app in two ways:
* From the Flipper Zero menu
* Remotely from your PC using the CLI (command-line interface)
Let's explore them both.
### How to run a script from Flipper Zero's menu
1. Go to **Apps → Scripts** in your Flipper Zero's menu. Here, you'll see a list of scripts located in the `SD Card/apps/Scripts/` folder.
2. Select the script you want to run.
3. Press the **OK** button to run the script.
\image html js_first_app_on_fz.jpg width=500
The Flipper Zero screen will display the strings with the specified delay, as defined by the `print()` and `delay()` functions.
### How to run script using CLI
The command-line interface (CLI) is a text-based interface that lets you control your Flipper Zero from your computer, including running scripts. Running JavaScript apps via CLI is useful for debugging, as it lets you write and test code remotely, without switching between your PC and the device.
To run the script via CLI:
1. Connect your Flipper Zero to your PC via USB.
2. Access the CLI using one of the [recommended methods](https://docs.flipper.net/development/cli#HfXTy).
3. Enter the `js path` command, replacing `path` with the path to the script file on your Flipper Zero:
\code{.sh}
js /ext/apps/Scripts/first_app.js
\endcode
\image html js_first_app_on_cli.jpg width=700
As you can see, unlike running JavaScript apps from the Flipper Zero UI, all output from the `print()` function is sent to the CLI, not the device screen.
**Next step:** [Developing apps using JavaScript SDK](#js_developing_apps_using_js_sdk)