v0.7.1 (build 925)

Virtual filesystem for bundled content. Multiple sources can be mounted into a single namespace and accessed uniformly from scripts.

Import #

import * as Assets from 'Koya/Assets';

Functions #

getText
Returns the file contents as a UTF-8 string.
  • Path is resolved by the internal asset system (e.g., /rom/...).
  • Throws if the asset does not exist.
getText(path: string): Promise<string>
getBinary
Returns the file contents as raw bytes (0-255 values).
  • Path is resolved by the internal asset system (e.g., /rom/...).
  • Throws if the asset does not exist.
getBinary(path: string): Promise<number[]>
readDir
Returns all file paths under the provided virtual directory path.
  • The lookup is recursive for nested subdirectories.
  • Useful for discovering available assets at runtime.
readDir(path: string): Promise<string[]>
mount
Mounts a host directory or ZIP archive while the application is running.
  • The destination defaults to /rom.
  • Relative sources use the process working directory; use mountAdjacent for application sidecars.
  • The Promise rejects when the source or virtual destination is invalid.
mount(source: string, destination?: string): Promise<boolean>
mountAdjacent
Mounts a sidecar directory or ZIP relative to the application executable.
  • The destination defaults to /rom.
  • The source must be relative and cannot contain parent traversal.
  • Use this during bootstrap for plugin and data packs distributed beside the executable.
mountAdjacent(source: string, destination?: string): Promise<boolean>

Virtual filesystem #

  • If the running executable has a ZIP appended to it, Koya automatically mounts that application archive at /rom before loading the bootstrap.
  • No system, user, or working-directory assets are mounted implicitly. --default-mounts opts into the legacy XDG, user configuration, and ./assets locations.
  • -m can be repeated for explicit startup mounts. Explicit mounts and bootstrap-time mounts override the application archive on collisions.
  • Once mounted, access assets via the virtual path, e.g. /rom/index.js, /rom/shaders/vertex.spv, /rom/images/logo.png.
./bin/koya -m ./assets -m ./extra_pack.zip -i apps/demo.js
./bin/koya --default-mounts -i index.js

Archive and executable mounts #

A mount source may be a directory, a ZIP archive, or an executable with an unmodified ZIP appended to it. At startup Koya discovers its own executable and automatically mounts an appended application archive at /rom. A minimal embedded bootstrap can then mount independently distributed plugin or data-pack ZIPs from beside the executable.

# The appended archive contains /rom/index.js
./my-koya-app -- /path/to/application/input
  • ZIP64 archives, empty files, and long member names are supported.
  • Archive paths are normalized into the virtual namespace; absolute paths and traversal members such as ../file are rejected.
  • Native hosts can use Helix::Assets::mountArchiveRange when the ZIP occupies a known byte range inside a larger file, including files with data after the archive.
  • Later mounts override earlier mounts when their virtual paths collide.
import * as Assets from 'Koya/Assets';

// Resolve from the executable directory, independent of the working directory.
await Assets.mountAdjacent('plugins/markdown.zip', '/rom/plugins/markdown');
await Assets.mountAdjacent('plugins/themes.zip', '/rom/plugins/themes');

Packaging a Koya application #

Package the application bootstrap and essential assets into one ZIP, then append that ZIP byte-for-byte to a copy of the Koya runtime. The ZIP root becomes /rom, so index.js must be at the archive root rather than inside an extra app/ directory. Keep independently updated plugins as sidecar ZIPs.

source/
├── app/
│   ├── index.js
│   ├── shell/
│   └── images/
└── plugins/
    ├── markdown/
    │   ├── manifest.json
    │   └── index.js
    └── themes/
        ├── manifest.json
        └── index.js

Build the Linux distribution #

set -eu

APP_ROOT=/path/to/source
KOYA_RUNTIME=/path/to/koya
DIST=/path/to/dist/MyApp
PACKAGE_TMP=$(mktemp -d)
APP_ZIP=$PACKAGE_TMP/application.zip

mkdir -p "$DIST/plugins"

# Archive contents, not the app directory itself: index.js must be at ZIP root.
(
  cd "$APP_ROOT/app"
  zip -q -r "$APP_ZIP" .
)

install -m755 "$KOYA_RUNTIME" "$DIST/my-app"
dd if="$APP_ZIP" of="$DIST/my-app" oflag=append conv=notrunc status=none

# Create independently replaceable sidecar packs.
(
  cd "$APP_ROOT/plugins/markdown"
  zip -q -r "$DIST/plugins/markdown.zip" .
)
(
  cd "$APP_ROOT/plugins/themes"
  zip -q -r "$DIST/plugins/themes.zip" .
)

rm -rf "$PACKAGE_TMP"

Mount sidecar plugins #

// app/index.js inside the executable archive
import * as Assets from 'Koya/Assets';

export default async function main(args)
{
    await Assets.mountAdjacent('plugins/markdown.zip', '/rom/plugins/markdown');
    await Assets.mountAdjacent('plugins/themes.zip', '/rom/plugins/themes');

    // Dynamically import plugin code only after its pack is mounted.
    const markdown = await import('/rom/plugins/markdown/index.js');
    await markdown.activate?.();
}

Ship the resulting directory as a tarball or through a platform package. Native Module/* libraries cannot be loaded directly from ZIP data: place libhx-<name>.so beside the application executable or provide a real directory with -n. The target system must still provide Koya's dynamically linked system libraries unless the chosen distribution format bundles them.

MyApp/
├── my-app                    # Koya runtime + appended application ZIP
├── libhx-editor.so           # optional native module
└── plugins/
    ├── markdown.zip
    └── themes.zip
  • Create an ordinary ZIP; do not rebase it with zip -A.
  • Build the final executable before appending the archive. Any later relink replaces the appended data.
  • Run the packaged application from a different working directory to verify that it relies only on self-discovery and mountAdjacent.
  • Later plugin mounts override earlier files, so mount base packs before optional patches.
const cfg = await Assets.getText('/rom/config.json');

What uses /rom #

All engine- and script-level asset paths point into the mounted namespace:

  • Helix/UserInterface — setTexture(windowId, id, '/rom/...'), setShader(windowId, id, { vert: '/rom/shaders/....spv', frag: '/rom/shaders/....spv' }), setTextFont(windowId, id, '/rom/fonts/....ttf')
  • Shaders — Use SPIR-V files under /rom/shaders/*.spv for custom pipelines.
  • Images and atlases — Box/panel textures and sprite atlases load from /rom/... paths.
  • Bootstrap and modules — The -i bootstrap file is resolved under /rom, and your JS can freely read other /rom files.

This keeps scripts portable: swap or add mounts (-m) to switch themes, assets, or shader packs without changing code.

Examples #

Load text and binary assets from the virtual filesystem and discover available files at runtime. This covers the three common patterns: reading structured data (JSON config), loading raw bytes (image/binary), and enumerating a directory to find available resources.

import * as Assets from 'Koya/Assets';
import * as Log from 'Helix/Log';

const shader = await Assets.getText('/rom/shaders/vertex.glsl');
await Log.info('Shader length=' + shader.length);

const configJson = await Assets.getText('/rom/config.json');
const config = JSON.parse(configJson);
await Log.debug('Config keys: ' + Object.keys(config).join(','));

const bytes = await Assets.getBinary('/rom/textures/logo.png');
await Log.debug('Binary size=' + bytes.length);

const files = await Assets.readDir('/rom/shaders');
await Log.debug('Shader files: ' + files.join(', '));

Notes #

  • Paths that do not exist will throw from the engine side; ensure presence in the asset bundle.