Module System
Modules are external scripts that integrate into the Oidis Builder task lifecycle. They can run before or after any built-in task, or completely replace a built-in task.
What Is a Module?
A module is any script (Node.js, Python, shell, or other interpreter) registered with the builder.
Modules are executed by the ModulesResolver at the appropriate point in the task chain.
Support for multiple interpreters does not justify adding another stack. Follow the
technology selection policy:
prefer the project's existing runtime and obtain approval for a justified exception.
Use cases:
- Run a custom code generator before TypeScript compilation
- Post-process the build output after packaging
- Replace a built-in task with project-specific logic
- Run project-specific linting tools alongside the standard ones
Registering Modules
Modules are registered in two places:
1. package.conf.jsonp — Project Modules
In the modules field of your project config:
"modules": {
"my-codegen": {
"script": "resource/scripts/codegen.js",
"args": ["--verbose"],
"interpreter": "node",
"overrideTask": false,
"events": {
"before": "compile",
"after": []
}
},
"my-postbuild": {
"script": "resource/scripts/postbuild.js",
"events": {
"after": ["compress", "target-deploy"]
}
}
}
2. resource/configs/modules.register.json — File-Based Registration
An alternative JSON file (without JSONP wrapper) for registering modules:
{
"my-hook": {
"script": "resource/scripts/my-hook.js",
"events": {
"before": ["typescript"],
"after": "build-check"
}
}
}
The builder loads modules.register.json from:
<builder-install>/resource/configs/modules.register.json— global hooks<project-root>/resource/configs/modules.register.json— project-specific hooks
Both sources are merged.
Module Configuration Fields
| Field | Type | Required | Description |
|---|---|---|---|
script | string | Yes | Path to the script file. Relative paths are resolved from the project root. Absolute paths and ~/ paths are used as-is. |
args | string[] | No | Default arguments passed to the script |
interpreter | string | No | Script interpreter (node, python, bash, etc.). If omitted, determined from script extension. |
overrideTask | boolean | No | If true, this module replaces the built-in task with the same name. Default: false. |
events.before | string | string[] | No | Task name(s) to hook before. The module runs before these tasks. |
events.after | string | string[] | No | Task name(s) to hook after. The module runs after these tasks. |
How Hooks Work
When TasksResolver resolves a task, it asks ModulesResolver for the events registered for that task.
The final execution set becomes:
[...before-modules] → task → [...after-modules]
Example: If my-codegen has events.before: "compile", then running oidis build dev produces:
solution-loader
...
my-codegen ← before compile
compile:dev
...
Multiple tasks in events:
"events": {
"before": ["typescript", "sass-compile"],
"after": "compress"
}
The module runs before both typescript AND before sass-compile, plus after compress.
Task:option hook matching:
When a task is run as typescript:source, hooks registered for typescript also fire (the base name is matched).
Overriding Built-in Tasks
Set overrideTask: true to replace a built-in task entirely:
"modules": {
"typescript": {
"script": "resource/scripts/custom-ts.js",
"overrideTask": true
}
}
When a module overrides a task, the original built-in task does not run at all. Use this only when you need to completely replace the builder's implementation.
If a module name matches a built-in task name but overrideTask is false, the module is registered as a normal hook and the builder warns:
Module alias for "typescript" can not be created.
Script API
Modules are executed via ScriptHandler. The script receives:
__owner— the name of the task that triggered this module
For Node.js modules, the script is executed via vm.runInContext() (Node.js VM module) inside a
sandboxed context — not via require(). This is an important distinction with practical consequences.
Script Execution Context
Scripts run inside a Node.js VM sandbox, not as CJS modules. The following globals are available:
| Global | Notes |
|---|---|
require | Standard Node.js require |
process | Full process object |
Buffer | Node.js Buffer |
console | Standard console |
setTimeout / clearTimeout | Timers |
setInterval / clearInterval | Timers |
module.exports | Available but not used by builder |
| Oidis namespaces | All Io.* objects from globalThis |
__filename / __dirname | Builder binary paths (not project paths) |
return is illegalIn a CJS module (require()d file), top-level return is valid because Node.js wraps the
file in a function. In a VM script, there is no such wrapper — top-level return causes:
SyntaxError: Illegal return statement
Always wrap script logic in an IIFE to enable early-exit via return:
// ✅ Correct — IIFE allows return inside the function
"use strict";
(function () {
const fs = require("fs");
if (!fs.existsSync("required-file")) {
console.log("[my-module] Prerequisite missing, skipping.");
return; // ← valid: inside a function
}
// ... rest of logic
}());
// ❌ Incorrect — top-level return throws SyntaxError: Illegal return statement
"use strict";
const fs = require("fs");
if (!fs.existsSync("required-file")) {
return; // ← SyntaxError in vm.runInContext()
}
Example Node.js module script (with IIFE):
// resource/scripts/my-codegen.js
"use strict";
(function () {
const fs = require("fs");
const path = require("path");
// Custom code generation logic
const outputDir = path.join(process.cwd(), "source/generated");
fs.mkdirSync(outputDir, { recursive: true });
// Generate a file
fs.writeFileSync(
path.join(outputDir, "GeneratedModule.ts"),
"// Auto-generated\nexport const VERSION = \"1.0\";\n"
);
console.log("Code generation complete.");
}());
Returning values from a module:
The value of the last evaluated expression is returned by vm.runInContext() and logged in debug mode.
Module Execution via CLI
Modules registered as non-overriding hooks are also accessible directly as tasks using the
module: prefix, or by their registered name as an alias:
oidis module:my-codegen # run module directly
oidis my-codegen # alias form (if not overriding)
When called directly with an option, the option is passed as the first argument override:
oidis my-codegen:verbose # args become ["verbose"]
Passing Arguments
Arguments can be provided in the module config or overridden at call time:
"modules": {
"my-tool": {
"script": "resource/scripts/my-tool.js",
"args": ["--default-mode"]
}
}
When the module is called directly with an option (e.g., my-tool:fast), the option string replaces
the args array for that invocation.
Load Order
Modules are loaded and merged in this order:
modulesfield frompackage.conf.jsonp(normalized relative to project root)modulesfield from builder's globalcommons.jsonp(normalized relative to builder root)<builder>/resource/configs/modules.register.json<project>/resource/configs/modules.register.json
Later registrations override earlier ones for the same module name.
Practical Examples
Run a Swagger code generator before compilation
"modules": {
"swagger-codegen": {
"script": "resource/scripts/generate-api.js",
"args": ["--spec", "resource/api/openapi.yaml", "--out", "source/typescript/generated"],
"events": {
"before": "compile"
}
}
}
Copy extra files after packaging
"modules": {
"copy-extras": {
"script": "resource/scripts/copy-extras.js",
"events": {
"after": "compress"
}
}
}
Add a custom linter alongside the standard ones
"modules": {
"my-custom-lint": {
"script": "resource/scripts/my-linter.js",
"events": {
"after": "test"
}
}
}
Notify Slack on successful prod deploy
"modules": {
"slack-notify": {
"script": "resource/scripts/slack-notify.js",
"args": ["--channel", "#builds"],
"events": {
"after": "deploy:prod"
}
}
}
The task:option form works in events too — "after": "deploy:prod" only fires after deploy:prod, not after deploy:dev.
See also: Build Process | Architecture Overview | Configuration