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';
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 | |
|---|---|
title | the dialog's title |
defaultPath | starting directory, or for saveFile the suggested name |
confirmLabel | the accept button's text |
multiple | openFile 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.