Quick Start (Manual Setup)
Besides using the scaffold, you can also write a minimal working SJMCL extension by hand.
1. Create the Folder
org.example.hello/
├─ sjmcl.ext.json
├─ icon.png
└─ frontend/
└─ index.jsThe folder name can match the extension identifier for easier debugging and packaging.
2. Write sjmcl.ext.json
{
"identifier": "org.example.hello",
"name": "Hello Extension",
"description": "My first SJMCL extension.",
"version": "0.1.0",
"minimalLauncherVersion": "0.8.0",
"frontend": {
"entry": "frontend/index.js"
}
}Key points:
identifiermust be namespacednamemust not be emptyversionshould be semverminimalLauncherVersionshould match your real requirementfrontend.entryis relative to the extension root
3. Write the Frontend Entry
(function registerHelloExtension(factory) {
const token = document.currentScript?.dataset?.extensionToken || "";
if (!token) {
throw new Error("Missing extension activation token");
}
window.registerExtension(factory, token);
})(function (api) {
const React = api.React;
const { Badge, Button, HStack, Text, VStack } = api.ChakraUI;
function HelloWidget() {
const host = api.getHostContext();
const hostData = api.useHostData();
const [count, setCount] = host.state.useExtensionState("count", 0);
return React.createElement(
VStack,
{ align: "stretch", spacing: 3 },
React.createElement(
HStack,
{ justify: "space-between", align: "center" },
React.createElement(Text, { fontWeight: "bold" }, "Hello Extension"),
React.createElement(
Badge,
{ colorScheme: "green", variant: "subtle" },
hostData.selectedInstance?.name || "No Instance"
)
),
React.createElement(
Text,
{ fontSize: "sm" },
"Current player: ",
hostData.selectedPlayer?.name || "None"
),
React.createElement(
Button,
{
size: "sm",
onClick: function () {
setCount((value) => value + 1);
},
},
"Clicked ",
String(count),
" times"
)
);
}
return {
homeWidget: {
title: "Hello",
defaultWidth: 320,
minWidth: 260,
Component: HelloWidget,
},
};
});This covers the basic rules:
- read the activation token from
document.currentScript - call
window.registerExtension(factory, token) - build UI with injected
ReactandChakraUI - use
api.useHostData()for reactive host-owned data - use
host.state.useExtensionState()for extension-owned state
Besides the home widget, you can also create custom pages and standalone windows. See UI Contributions.
4. Optionally Add a Settings Page
function SettingsPage() {
const React = api.React;
const { Box, Text } = api.ChakraUI;
return React.createElement(
Box,
null,
React.createElement(Text, null, "Hello from extension settings.")
);
}
return {
homeWidget: {
title: "Hello",
Component: HelloWidget,
},
settingsPage: {
Component: SettingsPage,
},
};If settingsPage is returned, SJMCL exposes a dedicated settings entry for the extension.
5. Package It
SJMCL accepts both .sjmclx and .zip. They are both zip archives in practice.
zip -r org.example.hello.sjmclx org.example.helloYou can also generate org.example.hello.zip, which the launcher can recognize as well.
6. Import It into SJMCL
- Open the launcher.
- Go to Settings.
- Open the Extensions page.
- Click Add and select the package.
Newly added extensions are enabled by default. If your extension provides a home widget or page, it will load automatically after the extension list is refreshed.
Debugging
- log through
host.actions.logger - start with one
homeWidget - keep persistent UI state in
useExtensionState() - resolve assets with
api.resolveAssetUrl() - call
host.actions.reloadSelf()when you need a clean reload
If the extension import fails, check these items first:
sjmcl.ext.jsonexists at the extension rootidentifieris validfrontend.entrypoints to an existing pathminimalLauncherVersionis not higher than the launcher version- the entry script actually calls
window.registerExtension(...)
