Skip to main content

Structure & Sharing

One entry, any number of modules

A module is registered by evaluating a defineModule call, so a script can declare as many as it likes — in the entry file itself, or in files the entry imports:

// src/index.ts
import { defineModule, ModuleCategory } from "@protohax/userscript";

defineModule(
{ name: "AutoSprint", category: ModuleCategory.Movement },
{
speed: { type: "number", def: 1, min: 0, max: 5, step: 0.1 },
},
(ctx) => {
ctx.on("tick", () => ctx.session.entityState.localPlayer.strafe(ctx.options.speed.value, 1));
},
);

Splitting across files is a matter of taste — the bundler inlines everything the entry reaches either way. If a file only registers a module and exports nothing, import it for its side effect:

import "./modules/auto-sprint";

What you should not do is add a second bundler entry point: that produces a second output file.

Importing another script

A phaxuser: import reaches a different file in the client's scripts folder, resolved at load time the same way @protohax/userscript is. The path is relative to that folder:

// <scripts>/lib/math.js ->
import { clamp } from "phaxuser:lib/math.js";

// <scripts>/shared.js ->
import shared from "phaxuser:shared.js";

Use it for code you want to share between scripts — a helper library, a common config — rather than for splitting one script across files. Everything inside your own src/ should be a normal relative import so it gets bundled; phaxuser: is for reaching things that ship as their own file.

Two consequences for the build:

Keep the specifier external. Like the facade, it must survive into the output — bundle it and there is nothing left to resolve:

// rollup.config.mjs
external: ["@protohax/userscript", /^phaxuser:/],

Declare it for TypeScript, which can't resolve the scheme on its own. A shorthand ambient declaration types every such import as any:

// src/phaxuser.d.ts
declare module "phaxuser:*";

For a library you control, declare its shape too — the specific declaration wins over the wildcard, and you get real type checking back:

declare module "phaxuser:lib/math.js" {
export function clamp(value: number, min: number, max: number): number;
}

The target script exports what any ES module does; make sure the file you are importing actually ships to the scripts folder alongside the one importing it.