NEOLearn

Modules

Share functions, classes and constants between a game's scripts with modules, in JavaScript or Python.

Updated October 17, 2026

On this page

A module is a script file that holds shared code instead of a world class. Your scripts (and other modules) import names from it. Modules live in assets/scripts with your scripts and use the game's one language.

A JavaScript module#

A module exports names. It has no export default:

paths.js
// A point on a square of side 8 around the origin, `t` laps per second.
export function square(t) {
  const u = (t % 1) * 4; // 0..4 along the four sides
  const side = Math.floor(u);
  const f = u - side;
  const a = -4 + 8 * f;
  if (side === 0) return { x: a, y: 1, z: -4 };
  if (side === 1) return { x: 4, y: 1, z: a };
  if (side === 2) return { x: -a, y: 1, z: 4 };
  return { x: -4, y: 1, z: -a };
}

A script imports it by name, with ./ in front:

platform.js
import { World, on } from "neo";
import { square } from "./paths";

export default class PlatformMover extends World {
  state = { lap: 0 };

  @on("tick")
  move(dt) {
    const platform = this.query("Platform")[0];
    if (!platform) return;
    this.state.lap += (dt / 1000) * platform.speed;
    platform.position = square(this.state.lap);
  }
}

What a JavaScript module can export#

FormExample
A functionexport function lerp(a, b, t) { ... } (also async function, function*)
A classexport class Board { ... }
A constant or variableexport const HALF = 0.5; (also let, var)
A list of namesexport { clamp01, hidden as one };

Destructuring exports (export const { a, b } = ...) and export default aren't supported in modules.

Importing#

FormExample
Named importsimport { lerp, HALF } from "./maths";
Renamedimport { lerp as mix } from "./maths";
The whole moduleimport * as maths from "./maths";
With the extensionimport { clamp01 } from "./util.js"; (the same module as "./util")

Write each import on one line, at the top level. Modules can import neo and other modules, and can use @on and other decorators in classes they export.

A Python module#

In Python, a module's public names are its top-level functions, classes and variables whose names don't start with _:

util.py
def clamp(x, lo, hi):
    return min(hi, max(lo, x))

BONUS = 7
scoring.py
from .util import clamp, BONUS

def points(streak):
    return clamp(streak * 3, 0, 20) + BONUS

class Board:
    def __init__(self):
        self.rows = []

    def add(self, name, pts):
        self.rows.append((name, pts))
        self.rows.sort(key=lambda r: (-r[1], r[0]))
        return len(self.rows)

    def top(self):
        return self.rows[0][0] if self.rows else None

A script imports names or the whole module, with a leading .:

game.py
from neo import World, on, sleep
from .scoring import points, Board
from . import util

board = Board()

class Game(World):
    state = {"scores": {}, "top": None, "n": 0}

    @on("scored")
    async def scored(self, name, streak):
        p = points(streak)
        self.state.scores = {**self.state.scores, name: self.state.scores.get(name, 0) + p}
        self.state.n = board.add(name, p)
        await sleep(0.032)
        self.state.top = board.top()
        self.emit("leader", {"name": board.top(), "bonus": util.BONUS})
  • from .name import a, b and from . import name are the two forms. Modules are flat: from ..x import y and from .pkg.mod import y are compile errors.
  • from .name import * isn't available.
  • Importing a name the module doesn't export raises ImportError.
  • Imports go at the top of the file.

Rules for both languages#

  • Only neo and your game's modules. Anything else, such as fs, a URL or ../x, is a compile error. Python can also import NEO's standard modules (see Python scripts).
  • One language. A JavaScript script can't import a Python module, or the other way round.
  • Frozen exports. You can't reassign or add names on an imported module: util.clamp01 = null throws.
  • One copy per script. Each script evaluates its own copy of every module it imports, in dependency order, before the script itself. Module-level variables (like board above) belong to that one script and are not shared with other scripts.
  • No cycles. If two modules import each other, loading fails with a message naming the cycle.
  • Not state. Module-level variables are not part of this.state: they start fresh when a script reloads and aren't in the state hash. Keep anything the game must remember in state.

Updating a module while the game runs#

When you save a module, scripts loaded or reloaded after that import the new version. Scripts already running keep the version they started with, so reload the scripts that import it. The Scripts panel tells you when a module was updated. See The Scripts panel.

If your game adds scripting through Rust, pass modules to the plugin as (name, source) pairs:

Rust
NeoScriptPlugin { modules: vec![("paths".into(), PATHS_JS.into())], .. }

See also#