Migrating from Neutralinojs
Move your Neutralinojs app to LightShell — what changes, what stays the same.
Neutralinojs and LightShell share a similar philosophy: use the system webview, provide a lightweight runtime, and avoid bundling a browser engine. This makes migration straightforward. The core ideas translate directly — the main changes are in API naming, security model, and IPC transport.
Concept Mapping
Section titled “Concept Mapping”| Neutralinojs | LightShell | Notes |
|---|---|---|
neutralino.config.json | lightshell.json | App configuration |
Neutralino.* | lightshell.* | Global API object |
neutralino.js client library | Built-in (auto-injected) | No script tag needed |
Neutralino.init() | Not needed | LightShell initializes automatically |
| Localhost WebSocket IPC | Unix domain socket IPC | More secure |
resources/ directory | src/ directory | App source files |
neu build | lightshell build | Build command |
neu run | lightshell dev | Development command |
neu create | lightshell init | Project scaffolding |
| ~2MB binary + resources | ~5MB self-contained binary | Resources embedded in binary |
| No permission system | Granular permissions | Optional restricted mode |
API Mapping
Section titled “API Mapping”File System
Section titled “File System”// Neutralinojsawait Neutralino.filesystem.readFile('/path/to/file')await Neutralino.filesystem.writeFile('/path/to/file', 'content')await Neutralino.filesystem.readDirectory('/path')await Neutralino.filesystem.createDirectory('/path/to/dir')await Neutralino.filesystem.removeFile('/path/to/file')await Neutralino.filesystem.removeDirectory('/path/to/dir')
// LightShellawait lightshell.fs.readFile('/path/to/file')await lightshell.fs.writeFile('/path/to/file', 'content')await lightshell.fs.readDir('/path')await lightshell.fs.mkdir('/path/to/dir')await lightshell.fs.remove('/path/to/file')await lightshell.fs.remove('/path/to/dir') // same method for files and dirsDialogs
Section titled “Dialogs”// Neutralinojsconst entries = await Neutralino.os.showOpenDialog('Open File', { filters: [{ name: 'Text', extensions: ['txt'] }]})const path = entries[0]
const savePath = await Neutralino.os.showSaveDialog('Save', { filters: [{ name: 'Text', extensions: ['txt'] }]})
await Neutralino.os.showMessageBox('Title', 'Message')
// LightShellconst path = await lightshell.dialog.open({ title: 'Open File', filters: [{ name: 'Text', extensions: ['txt'] }]})
const savePath = await lightshell.dialog.save({ title: 'Save', filters: [{ name: 'Text', extensions: ['txt'] }]})
await lightshell.dialog.message('Title', 'Message')Note: Neutralino.os.showOpenDialog returns an array of paths. lightshell.dialog.open returns a single path (or an array if multiple: true is set).
Clipboard
Section titled “Clipboard”// Neutralinojsawait Neutralino.clipboard.writeText('Hello')const text = await Neutralino.clipboard.readText()
// LightShellawait lightshell.clipboard.write('Hello')const text = await lightshell.clipboard.read()Process Execution
Section titled “Process Execution”// Neutralinojs -- no scoping, full shell accessconst result = await Neutralino.os.execCommand('ls -la /tmp')console.log(result.stdOut)
// LightShell -- direct execution, no shell, optional scopingconst result = await lightshell.process.exec('ls', ['-la', '/tmp'])console.log(result.stdout)Key difference: Neutralinojs passes commands through the shell (sh -c), which allows shell injection. LightShell executes commands directly via exec.Command, which prevents shell injection entirely. Pipe characters, semicolons, and backticks are treated as literal strings, not shell operators.
Storage
Section titled “Storage”// Neutralinojs -- file-based key-valueawait Neutralino.storage.setData('user', JSON.stringify({ name: 'Alice' }))const raw = await Neutralino.storage.getData('user')const user = JSON.parse(raw)
// LightShell -- JSON-native key-value storeawait lightshell.store.set('user', { name: 'Alice' }) // auto-serializedconst user = await lightshell.store.get('user') // auto-deserializedLightShell’s store automatically handles JSON serialization. You pass objects directly instead of manually calling JSON.stringify and JSON.parse.
Window Management
Section titled “Window Management”// Neutralinojsawait Neutralino.window.setTitle('My App')await Neutralino.window.setSize({ width: 800, height: 600 })await Neutralino.window.minimize()await Neutralino.window.maximize()await Neutralino.window.setFullScreen()
// LightShellawait lightshell.window.setTitle('My App')await lightshell.window.setSize(800, 600)await lightshell.window.minimize()await lightshell.window.maximize()await lightshell.window.fullscreen()System / OS Info
Section titled “System / OS Info”// Neutralinojsconst info = await Neutralino.computer.getOSInfo()const platform = NL_OS // global constant
// LightShellconst platform = await lightshell.system.platform() // "darwin" or "linux"const arch = await lightshell.system.arch() // "arm64" or "x64"const homeDir = await lightshell.system.homeDir()App Lifecycle
Section titled “App Lifecycle”// NeutralinojsNeutralino.init()Neutralino.events.on('windowClose', () => { Neutralino.app.exit()})
// LightShell -- no init needed, quit is explicit// App starts automatically when index.html loadslightshell.app.quit() // call when you want to exitOpen External URLs
Section titled “Open External URLs”// Neutralinojsawait Neutralino.os.open('https://example.com')
// LightShellawait lightshell.shell.open('https://example.com')Notifications
Section titled “Notifications”// Neutralinojsawait Neutralino.os.showNotification('Title', 'Body')
// LightShellawait lightshell.notify.send({ title: 'Title', body: 'Body' })System Tray
Section titled “System Tray”// Neutralinojsawait Neutralino.os.setTray({ icon: '/resources/icon.png', menuItems: [ { id: 'show', text: 'Show' }, { id: 'quit', text: 'Quit' } ]})Neutralino.events.on('trayMenuItemClicked', (e) => { if (e.detail.id === 'quit') Neutralino.app.exit()})
// LightShellawait lightshell.tray.set({ tooltip: 'My App', menu: [ { label: 'Show', id: 'show' }, { label: 'Quit', id: 'quit' } ]})lightshell.tray.onClick((data) => { if (data.id === 'quit') lightshell.app.quit()})Migration Steps
Section titled “Migration Steps”1. Create lightshell.json
Section titled “1. Create lightshell.json”Translate your neutralino.config.json to lightshell.json:
Before (neutralino.config.json):
{ "applicationId": "com.example.myapp", "defaultMode": "window", "port": 0, "url": "/resources/", "nativeAllowList": [ "app.*", "os.*", "filesystem.*", "clipboard.*", "window.*" ], "modes": { "window": { "title": "My App", "width": 1000, "height": 700, "minWidth": 400, "minHeight": 300 } }}After (lightshell.json):
{ "name": "my-app", "version": "1.0.0", "entry": "src/index.html", "window": { "title": "My App", "width": 1000, "height": 700, "minWidth": 400, "minHeight": 300 }}2. Move Source Files
Section titled “2. Move Source Files”Rename resources/ to src/ and update any internal paths:
# Neutralinojsresources/ index.html styles/ scripts/
# LightShellsrc/ index.html styles/ scripts/3. Remove Neutralinojs Client Library
Section titled “3. Remove Neutralinojs Client Library”In Neutralinojs, you include <script src="/__neutralino_globals.js"></script> in your HTML. Remove this tag. LightShell automatically injects its client library — no script tag needed.
<!-- Remove this line --><script src="/__neutralino_globals.js"></script>
<!-- Remove this line too --><script>Neutralino.init()</script>4. Replace API Calls
Section titled “4. Replace API Calls”Find and replace all Neutralino.* calls with lightshell.* equivalents using the mapping table above. The most common replacements:
Neutralino.filesystem.readFile -> lightshell.fs.readFileNeutralino.filesystem.writeFile -> lightshell.fs.writeFileNeutralino.os.showOpenDialog -> lightshell.dialog.openNeutralino.os.showSaveDialog -> lightshell.dialog.saveNeutralino.os.showMessageBox -> lightshell.dialog.messageNeutralino.os.execCommand -> lightshell.process.execNeutralino.clipboard.readText -> lightshell.clipboard.readNeutralino.clipboard.writeText -> lightshell.clipboard.writeNeutralino.storage.setData -> lightshell.store.setNeutralino.storage.getData -> lightshell.store.getNeutralino.window.setTitle -> lightshell.window.setTitleNeutralino.app.exit -> lightshell.app.quitNeutralino.os.open -> lightshell.shell.open5. Update Event Handling
Section titled “5. Update Event Handling”// Neutralinojs eventsNeutralino.events.on('windowClose', handler)Neutralino.events.on('trayMenuItemClicked', handler)Neutralino.events.on('ready', handler)
// LightShell -- use standard DOM events or lightshell event systemwindow.addEventListener('beforeunload', handler)lightshell.tray.onClick(handler)// No "ready" event needed -- code runs when the script loads6. Fix execCommand Calls
Section titled “6. Fix execCommand Calls”This is the change that requires the most attention. Neutralinojs runs commands through the shell as a single string. LightShell splits the command and arguments:
// Neutralinojs (shell string)await Neutralino.os.execCommand('grep -r "TODO" /project/src')
// LightShell (command + args array)await lightshell.process.exec('grep', ['-r', 'TODO', '/project/src'])If your Neutralinojs code uses shell features like pipes or redirection, you need to restructure:
// Neutralinojs -- uses shell pipeawait Neutralino.os.execCommand('cat file.txt | grep error | wc -l')
// LightShell -- run commands separately and process in JSconst result = await lightshell.process.exec('cat', ['file.txt'])const errorLines = result.stdout.split('\n').filter(l => l.includes('error'))const count = errorLines.length7. Test and Build
Section titled “7. Test and Build”lightshell dev # test in developmentlightshell build # produce the final binaryWhat You Gain
Section titled “What You Gain”| Improvement | Details |
|---|---|
| Security (IPC) | Unix domain socket with 0600 permissions vs localhost WebSocket. Neutralinojs’s WebSocket allows any process on the machine to connect to your app’s IPC channel. LightShell’s socket is owner-only. |
| Security (permissions) | Granular permission system with fs, process, and http scoping. Neutralinojs’s nativeAllowList is module-level only (all-or-nothing per namespace). |
| Security (process exec) | Direct execution prevents shell injection. Neutralinojs passes strings to sh -c. |
| Path traversal protection | Always-on symlink resolution and path validation. Not available in Neutralinojs. |
| CSP injection | Automatic Content Security Policy in production builds. |
| Built-in store | JSON-native key-value store with lightshell.store. Neutralinojs storage writes raw files. |
| CORS-free HTTP | lightshell.http.fetch bypasses CORS via the Go backend. Neutralinojs requires custom proxy setup. |
| AI-friendly errors | Structured error messages with fix instructions and doc links. |
| Self-contained binary | Single binary with embedded resources. Neutralinojs requires a binary + resources directory. |
What Changes
Section titled “What Changes”| Neutralinojs Feature | LightShell Equivalent |
|---|---|
nativeAllowList | permissions in lightshell.json (more granular) |
| Extensions (child processes) | lightshell.process.exec |
NL_PORT, NL_TOKEN globals | Not exposed (IPC is transparent) |
Neutralino.debug.log | console.log (shown in dev tools) |
| Custom cloud mode | Not available (desktop only) |
.neu project metadata | Not used |
Binary + resources.neu bundle | Single self-contained binary |
Platform Notes
Section titled “Platform Notes”Both Neutralinojs and LightShell use the system webview. If your Neutralinojs app already works with WebKitGTK on Linux and WKWebView on macOS, your UI code should work in LightShell without changes. The same browser engine quirks and limitations apply to both frameworks.
LightShell includes polyfills for APIs missing in older WebKitGTK versions (structuredClone, Array.prototype.group, Promise.withResolvers, Set methods). If your Neutralinojs app had workarounds for these gaps, you can remove them.