Skip to content

Developer Workflow

The app can be developed without a running ElyOS instance. The @elyos/sdk/dev package provides a Mock SDK that simulates all SDK services:

SDK serviceMock behavior
ui.toast()Writes to console.log
ui.dialog()window.confirm / window.prompt (in standalone mode)
data.set/get/delete()Uses localStorage (under devapp:{appId}: key prefix)
data.query()Returns empty array
remote.call()Configurable mock handler
i18n.t()Reads from the provided translation map
notifications.send()Writes to console.log

To start the dev server, you need an index.html file in the project root — this is what Vite looks for as the entry point. Projects generated by the CLI include this automatically.

Terminál
bun dev

The app will be available at http://localhost:5174. Vite hot reload automatically refreshes the browser on every save.

If you’re developing multiple apps simultaneously, the dev (Vite) and dev:server (static server) commands use port 5174 by default. This causes a conflict if both apps run at the same time.

The port can be overridden with the PORT environment variable:

Terminál
# First app — default port
bun run dev:server
# Second app — different port
PORT=5175 bun run dev:server

In the ElyOS Dev Apps loader, provide the URL accordingly: http://localhost:5175.

The Mock SDK (@elyos/sdk/dev) is a development package that simulates the real window.webOS SDK — without a running ElyOS instance. When the app runs in standalone mode (e.g. bun dev), window.webOS doesn’t exist yet, so MockWebOSSDK.initialize() creates and exposes it.

In src/main.ts this happens automatically:

src/main.ts
import { mount } from 'svelte';
import { MockWebOSSDK } from '@elyos/sdk/dev';
import App from './App.svelte';
async function initDevSDK() {
if (typeof window !== 'undefined' && !window.webOS) {
// Only runs when NOT in ElyOS
MockWebOSSDK.initialize({
// Translations for development
i18n: {
locale: 'en',
translations: {
en: { title: 'My App', welcome: 'Welcome!' },
hu: { title: 'Alkalmazás', welcome: 'Üdvözöljük!' }
}
},
// Simulated user and permissions
context: {
appId: 'my-app',
user: {
id: 'dev-user',
name: 'Developer',
email: 'dev@localhost',
roles: ['admin'],
groups: []
},
permissions: ['database', 'notifications', 'remote_functions']
}
});
}
}
async function init() {
await initDevSDK();
const target = document.getElementById('app');
if (target) mount(App, { target }); // Svelte 5: mount() — not new App()
}
init();

All configuration options for initialize():

OptionTypeDescription
i18n.localestringDefault language (e.g. 'en')
i18n.translationsRecord<string, Record<string, string>>Translation keys map per language
context.appIdstringSimulated app ID
context.userUserInfoSimulated logged-in user
context.permissionsstring[]Simulated permissions
data.initialDataRecord<string, unknown>Pre-populated localStorage data
remote.handlersRecord<string, Function>Mock server function handlers
assets.baseUrlstringAsset URL prefix

When ElyOS loads the app in production, window.webOS already exists (populated with the runtime SDK), so the if (!window.webOS) condition prevents the Mock SDK from running.

If you’re also testing server functions in standalone mode:

MockWebOSSDK.initialize({
remote: {
handlers: {
getServerTime: async () => ({
iso: new Date().toISOString(),
locale: new Date().toLocaleString('en-US')
}),
calculate: async ({ a, b, operation }) => {
if (operation === 'add') return { result: a + b };
throw new Error('Unsupported operation');
}
}
}
});

Standalone dev mode (Mock SDK) only tests the UI. If you want to test real SDK calls, database access, or server functions, the app must be loaded into a running ElyOS instance.

The process: build the app, start a static HTTP server, then load it into ElyOS via URL. There is no automatic hot reload — if you change the code, you need to rebuild and reopen the app window.

In the elyos-core monorepo root:

Terminál
# In .env.local, enable dev app loading:
# DEV_MODE=true
bun app:dev

ElyOS is available at http://localhost:5173 by default. Log in with an admin account.

In the app project folder:

Terminál
bun run build

This creates dist/index.iife.js — the file loaded by ElyOS.

Terminál
bun run dev:server

This starts the dev-server.ts Bun HTTP server at http://localhost:5174. The server serves files from the dist/ folder and the project root with CORS headers.

  1. Open ElyOS in the browser
  2. Start menu → App Manager
  3. Click “Dev Apps” in the left sidebar
  4. A URL input field appears with http://localhost:5174 as the default value
  5. Click the “Load” button

ElyOS fetches the manifest.json from the dev server, then loads the IIFE bundle and registers the app as a Web Component.

Terminál
# 1. Rebuild
bun run build
# 2. In ElyOS: close the app window, then reopen it
# (no need to press "Load" again — the app is already in the list)
Terminál
# Terminal 1 — ElyOS core
cd elyos-core && bun app:dev
# Terminal 2 — App build + server
cd my-app
bun run build # Create IIFE bundle
bun run dev:server # Start static server (http://localhost:5174)
# In ElyOS: App Manager → Dev Apps → Load → http://localhost:5174

The @elyos/sdk includes full TypeScript type definitions. The window.webOS type is automatically available:

// Automatic type — no import needed
const sdk = window.webOS!;
sdk.ui.toast('Hello!', 'success'); // ✅ autocomplete
sdk.data.set('key', { value: 123 }); // ✅ type checking
sdk.remote.call<MyResult>('fn', params); // ✅ generic return type

Explicit type import when needed:

import type { WebOSSDKInterface, UserInfo } from '@elyos/sdk/types';
const user: UserInfo = sdk.context.user;

The plugin uses Svelte 5 runes-based reactivity. The runes: true compiler option is enabled in vite.config.ts:

<script lang="ts">
const sdk = window.webOS!;
let count = $state(0);
let doubled = $derived(count * 2);
$effect(() => {
sdk.ui.toast(`Count: ${count}`, 'info');
});
</script>
<button onclick={() => count++}>
{count} (doubled: {doubled})
</button>

Language Switching in Standalone Mode (sidebar template)

Section titled “Language Switching in Standalone Mode (sidebar template)”

The sidebar template’s App.svelte includes a built-in language switcher button at the bottom of the sidebar — this only appears if the plugin defines multiple locales. In standalone mode (Mock SDK), the sdk.i18n.setLocale() call updates the Mock SDK’s internal state, and the {#key currentLocale} block remounts the active component.

When loaded into ElyOS, setLocale() calls the core i18n system, which switches the entire app’s language — not just the plugin’s language.

The plugin’s CSS is not automatically bundled into the JS bundle during IIFE build — Vite in lib mode extracts it into a separate .css file. If this file is not loaded, the plugin’s styles won’t appear at all in ElyOS.

When Vite builds an IIFE bundle, it puts the Svelte components’ CSS into a separate file (dist/sample-01-plugin.css). ElyOS only loads the JS bundle — not the CSS file. Result: the plugin’s styles are completely missing.

The solution is a custom Vite plugin in vite.config.ts that injects the generated CSS as a <style> tag at the beginning of the JS bundle:

vite.config.ts
function injectCssPlugin(): Plugin {
let cssContent = '';
return {
name: 'inject-css',
apply: 'build',
generateBundle(_, bundle) {
// Collect CSS file content and remove from bundle
for (const [fileName, chunk] of Object.entries(bundle)) {
if (fileName.endsWith('.css') && chunk.type === 'asset') {
cssContent += chunk.source as string;
delete bundle[fileName];
}
}
// Inject CSS at the beginning of the JS bundle
if (cssContent) {
for (const chunk of Object.values(bundle)) {
if (chunk.type === 'chunk' && chunk.fileName.endsWith('.js')) {
const injection = `(function(){var s=document.createElement('style');s.textContent=${JSON.stringify(cssContent)};document.head.appendChild(s);})();`;
chunk.code = injection + chunk.code;
break;
}
}
}
}
};
}

This plugin is already included in the vite.config.ts generated by create-elyos-app — no need to add it manually.

Even if the CSS loads, the core app’s Tailwind styles (base layer resets) may override the plugin’s styles. This behavior is the same in all loading modes (dev URL and installed .elyospkg).

Svelte scoped CSS generates button.svelte-xxxx selectors, but Tailwind’s button { ... } reset loads with higher specificity, overriding them.

Always define styles within your own container class, and avoid bare HTML tag selectors:

<!-- ❌ Bad — core styles will override -->
<style>
button {
border: 1px solid #ccc;
padding: 0.5rem 1rem;
}
</style>
<!-- ✅ Good — scoped within container class -->
<style>
.my-plugin button {
border: 1px solid #ccc;
padding: 0.5rem 1rem;
}
</style>

If core styles are overriding an element, all: revert restores the browser’s native style:

<style>
.my-plugin button {
all: revert;
cursor: pointer;
border: 1px solid #ccc;
border-radius: 0.25rem;
padding: 0.5rem 1rem;
background: white;
}
.my-plugin button:hover {
background: #f0f0f0;
}
</style>
RuleWhy
Include injectCssPlugin() in vite.config.tsWithout it, CSS won’t load in ElyOS at all
Scope within container class (.my-plugin button)Core Tailwind styles override bare tag selectors
Use all: revert when neededRestores browser native style
Give the root container a unique class nameAvoids conflicts with other plugins’ styles