Skip to main content

api

-, sidebar_position: 3 title: APIs;​

APIs

Components come from react-native exactly as they do on iOS and Android. basalt-core is only the few things with no React Native equivalent, because a phone has no such thing.

import {Platform, dialog, useDialog, resourcePath} from 'basalt-core';
note

basalt-core does not re-export react-native. It used to, and export * from 'react-native' compiles into a loop that reads every one of React Native's exports; most of which are lazy getters that run their module's top level when read. That loaded every deprecated component and then threw on DevMenu, which in an Expo app aborted App.js with "App entry not found".

Platform​

Platform.OS is 'linux', 'macos' or 'windows'. Metro picks the right one for the platform it is bundling for, the same mechanism React Native uses for its own.

Platform.select({macos: 'Cmd', default: 'Ctrl'});

select resolves this platform, then native, then default, the same shape as every other platform's, so Platform.select({native, default}) keeps working in shared code.

There is deliberately no desktop key. It would be useful, and it would also mean shared code behaves differently under React Native's own Platform.select on iOS, which is the kind of divergence a project about consistency should not introduce. If you want desktop-vs-mobile branching, write an explicit helper.

Version, isTesting and constants come from the host.

File dialogs​

The native open and save dialogs. React Native has no API for these because a phone has none.

const {canceled, paths} = await dialog.openFile({
title: 'Choose a clip',
multiple: true,
filters: [{name: 'Video', extensions: ['mp4', 'mov']}],
});
dialog.openFile(options?)one or more existing files
dialog.saveFile(options?)a path to write to, which may not exist yet
dialog.openFolder(options?)a directory; filters is ignored

All three resolve to {canceled: boolean, paths: string[]}, and all options are optional:

option
titlethe dialog's title
defaultPathstarting directory, or for saveFile the suggested name
confirmLabelthe accept button's text
multipleopenFile only
filters[{name, extensions}], with extensions bare: 'png', not '.png' and not 'image/png'

useDialog() is the same object from a hook, for code that prefers one.

Whether an existing file may be overwritten is the platform's question and it asks for itself: by the time saveFile resolves, the answer was yes. On a host with no dialog module the calls resolve to {canceled: true, paths: []} rather than throwing.

resourcePath()​

Where the files the build put beside your application are: Foo.app/Contents/Resources on macOS, beside the executable on Linux and Windows, which have no separate resources directory. A development run answers the build directory.

const bundled = `${resourcePath()}/fixtures/sample.mp4`;

This is here rather than worked out by each app because Basalt's packager decides the layout, and an app deriving it would be an app that has to know the shape of a bundle it did not build. Empty only when the running executable's path cannot be determined, which no supported desktop does.

Subprocesses​

npm install basalt-subprocess

A desktop front end for something else: a video editor driving ffmpeg, an editor running a language server: needs this, and React Native has no API for it.

import {spawn, kill, addOutputListener, addExitListener} from 'basalt-subprocess';

const pid = await spawn({
command: '"node" "server.js" --port 4790',
cwd: projectRoot,
env: {FFMPEG: ffmpegPath},
});

const output = addOutputListener(e => console.log(e.pid, e.stream, e.data));
const exit = addExitListener(e => console.log(e.pid, 'exited', e.code));
spawn(options)starts the command, resolves to a pid
kill(pid)boolean
isRunning(pid)boolean
isSupported()whether the native half is present
addOutputListener(fn){pid, stream, data}
addExitListener(fn){pid, code}

The command is handed to the platform's own shell, which is what gets a packaged application the PATH a person's login shell sets up: node, ffmpeg, and anything installed by asdf, nvm or Homebrew. Quote your arguments, as above.

spawn rejects only when the process could not be started at all. A command that starts and then fails is not a rejection: it reports through the exit listener, which is where an exit code belongs.

Notifications​

npm install basalt-notifications

This package has no JavaScript API of its own. It implements expo-notifications' contract natively, so the API you write against is expo-notifications, the same approach that made react-native-gesture-handler work here. React Native has nothing in core to be compatible with: PushNotificationIOS is a separate package and Android's is a library.

Each desktop uses its own mechanism, and what a desktop can do depends on the app having an identity, a bundle identifier on macOS, an AppUserModelID on Windows. The run command packages the binary into a real application on every run partly for this reason. Check getPermissionsAsync() before presenting anything, which is what expo-notifications' own documentation says to do: where a desktop cannot show one, it answers denied with a reason rather than failing at the call.

It ships separately from the core platform because it is permission-gated.