Building & Deployment#

Get your OneJS app running in standalone builds, WebGL, and stripped AOT targets like mobile.

How It Works#

When you build your Unity project, the JSRunnerBuildProcessor runs automatically and handles everything:

  1. Locates each JSRunner's app.js.txt bundle and assigns it as a TextAsset
  2. Copies assets/ to StreamingAssets/onejs/assets/ (images, fonts, data files)
  3. Extracts cartridge files into each runner's working directory (~/@cartridges/)

Styles (CSS Modules and Tailwind) are embedded in the JavaScript bundle, so they work automatically in builds.

Standalone Builds#

Works out of the box. Build normally from File > Build Settings.

The JavaScript bundle is stored as a TextAsset in the JSRunner's instance folder, which sits alongside your scene. The folder is named after the runner's GameObject (e.g. App), with a numbered suffix only on name collision (App_1, App_2):

Assets/Scenes/YourScene/
└── App/
    └── app.js.txt      # TextAsset used in builds

In the Editor, JSRunner loads from the filesystem (enabling hot reload). In builds, it uses this TextAsset.

WebGL Builds#

WebGL works with some differences:

  • JavaScript runs in the browser's V8/SpiderMonkey engine (JIT compiled, not QuickJS)
  • Uses the browser's native requestAnimationFrame
  • Full browser API access

Build normally and deploy to a web server.

Assets in Builds#

Image and data files from your assets/ folder are automatically copied to StreamingAssets/onejs/assets/ during the build. This means <Image src="logo.png" /> and loadImage("logo.png") work identically in Editor and builds.

The destination is flushed before each build, so deleted or renamed files won't linger as stale copies.

See the Asset Loading guide for more details.

Code Stripping (link.xml)#

On desktop (Mono) almost anything works. But AOT and IL2CPP targets (iOS, Android, WebGL) strip unused C# code, and OneJS calls into C# dynamically from JS through the CS proxy. The stripper can't see those calls, so types your JS relies on can get removed. A call that runs fine in the Editor then throws at runtime in the build:

[QuickJS] Method not found: System.IO.Path.Combine
[QuickJS] Type not found: UnityEngine.SomeType

"Method not found" means the type survived but that method was stripped. "Type not found" means the whole class was stripped. Both have the same fix: preserve it with a link.xml.

List the assembly of every class you call dynamically from JS in a link.xml placed anywhere under Assets/. Unity merges every link.xml in the project:

<linker>
    <assembly fullname="mscorlib" preserve="all" />
    <assembly fullname="OneJS.Runtime" preserve="all" />
    <assembly fullname="UnityEngine.CoreModule" preserve="all" />
    <assembly fullname="UnityEngine.PhysicsModule" preserve="all" />
    <assembly fullname="UnityEngine.TextRenderingModule" preserve="all" />
    <assembly fullname="UnityEngine.UIElementsModule" preserve="all" />
    <assembly fullname="UnityEngine.IMGUIModule" preserve="all" />
    <assembly fullname="Unity.Mathematics" preserve="all" />
</linker>

Preserve every assembly you reach from JS that your own C# doesn't already reference: mscorlib (the BCL: collections, System.IO, etc.), the UnityEngine.* modules whose APIs you use, Unity.Mathematics, and your own gameplay assembly. Start broad with whole-assembly preserve="all", then tighten to specific types later if build size matters. OneJS ships its own link.xml for its runtime internals, so you only handle your app's calls.

A few link.xml gotchas that trip people up the first time:

  • The file must be named link.xml, not linker.xml or links.xml.
  • The extension must be .xml, not something like .xml.txt.
  • The root node must be <linker>.

Quick way to confirm stripping is the culprit: set Player Settings > Other Settings > Managed Stripping Level to Minimal (or Disabled) and rebuild. If the error disappears, add the missing entries to your link.xml, then turn stripping back on. Don't ship with stripping off, it bloats the build.

Production Checklist#

Minify Your Code#

// esbuild.config.mjs
await esbuild.build({
    minify: true,
    treeShaking: true,
    drop: ["console", "debugger"],
})

Test Before Building#

Run npm run build and verify your app works in the Editor before making a Unity build.

Check Console#

Watch the Unity Console for build processor messages:

[JSRunner] Copied 3 asset file(s) to StreamingAssets for: App
[JSRunner] Build preprocessing complete. Processed 1 runner(s), created 1 asset(s), copied 3 asset file(s) to StreamingAssets.

Platform-Specific Code#

const isWebGL = typeof window !== "undefined"

function App() {
    useEffect(() => {
        if (isWebGL) {
            console.log("Running in browser")
        } else {
            console.log("Running in Unity")
        }
    }, [])
}

Troubleshooting#

"Bundle not found" in Build#

  • Ensure npm run build completed successfully
  • Verify app.js.txt exists in the instance folder
  • Check Unity Console for build processor errors

"Script error" in WebGL#

  • Open browser DevTools for the full error message
  • Check for CORS issues if loading external resources
  • Verify all imports are bundled (not externalized)

Assets Missing in Build#

  • Verify files are in the assets/ folder inside your working directory (~/)
  • Check the build log for "Copied N asset file(s) to StreamingAssets"
  • Ensure the JSRunner is enabled and its GameObject is active

Size Optimization#

Analyze your bundle. esbuild-visualizer needs a metafile, which the default config doesn't emit, so capture one first:

// esbuild.config.mjs
const result = await esbuild.build({ ...config, metafile: true })
fs.writeFileSync("meta.json", JSON.stringify(result.metafile))

Then visualize it:

npx esbuild-visualizer --metadata meta.json --open

Common optimizations:

  • Import only what you need from libraries
  • Remove unused exports with tree shaking
  • Use drop: ["console"] in production builds