Developer Workflow
Standalone Development (Mock SDK)
Section titled “Standalone Development (Mock SDK)”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 service | Mock 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 |
Starting the Dev Server
Section titled “Starting the Dev Server”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.
bun devThe app will be available at http://localhost:5174. Vite hot reload automatically refreshes the browser on every save.
Dev Server Port Configuration
Section titled “Dev Server Port Configuration”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:
# First app — default portbun run dev:server
# Second app — different portPORT=5175 bun run dev:serverIn the ElyOS Dev Apps loader, provide the URL accordingly: http://localhost:5175.
Mock SDK Initialization
Section titled “Mock SDK Initialization”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:
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():
| Option | Type | Description |
|---|---|---|
i18n.locale | string | Default language (e.g. 'en') |
i18n.translations | Record<string, Record<string, string>> | Translation keys map per language |
context.appId | string | Simulated app ID |
context.user | UserInfo | Simulated logged-in user |
context.permissions | string[] | Simulated permissions |
data.initialData | Record<string, unknown> | Pre-populated localStorage data |
remote.handlers | Record<string, Function> | Mock server function handlers |
assets.baseUrl | string | Asset 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.
Mocking Remote Calls
Section titled “Mocking Remote Calls”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'); } } }});Testing in a Running ElyOS Instance
Section titled “Testing in a Running ElyOS Instance”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.
Step 1 — Start ElyOS Core
Section titled “Step 1 — Start ElyOS Core”In the elyos-core monorepo root:
# In .env.local, enable dev app loading:# DEV_MODE=true
bun app:devElyOS is available at http://localhost:5173 by default. Log in with an admin account.
Step 2 — Build the App
Section titled “Step 2 — Build the App”In the app project folder:
bun run buildThis creates dist/index.iife.js — the file loaded by ElyOS.
Step 3 — Start the Static Dev Server
Section titled “Step 3 — Start the Static Dev Server”bun run dev:serverThis 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.
Step 4 — Load the App into ElyOS
Section titled “Step 4 — Load the App into ElyOS”- Open ElyOS in the browser
- Start menu → App Manager
- Click “Dev Apps” in the left sidebar
- A URL input field appears with
http://localhost:5174as the default value - 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.
Reloading After Changes
Section titled “Reloading After Changes”# 1. Rebuildbun 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)Full Dev Workflow Summary
Section titled “Full Dev Workflow Summary”# Terminal 1 — ElyOS corecd elyos-core && bun app:dev
# Terminal 2 — App build + servercd my-appbun run build # Create IIFE bundlebun run dev:server # Start static server (http://localhost:5174)
# In ElyOS: App Manager → Dev Apps → Load → http://localhost:5174TypeScript and Autocomplete
Section titled “TypeScript and Autocomplete”The @elyos/sdk includes full TypeScript type definitions. The window.webOS type is automatically available:
// Automatic type — no import neededconst sdk = window.webOS!;
sdk.ui.toast('Hello!', 'success'); // ✅ autocompletesdk.data.set('key', { value: 123 }); // ✅ type checkingsdk.remote.call<MyResult>('fn', params); // ✅ generic return typeExplicit type import when needed:
import type { WebOSSDKInterface, UserInfo } from '@elyos/sdk/types';
const user: UserInfo = sdk.context.user;Svelte 5 Runes in Plugins
Section titled “Svelte 5 Runes in Plugins”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.
Style Handling
Section titled “Style Handling”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.
The CSS Injection Problem
Section titled “The CSS Injection Problem”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:
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.
Specificity Conflicts
Section titled “Specificity Conflicts”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.
The Solution: Container-Based Scoping
Section titled “The Solution: Container-Based Scoping”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>all: revert — Resetting Core Styles
Section titled “all: revert — Resetting Core Styles”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>Summary
Section titled “Summary”| Rule | Why |
|---|---|
Include injectCssPlugin() in vite.config.ts | Without 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 needed | Restores browser native style |
| Give the root container a unique class name | Avoids conflicts with other plugins’ styles |