Skip to main content

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:

  1. <builder-install>/resource/configs/modules.register.json — global hooks
  2. <project-root>/resource/configs/modules.register.json — project-specific hooks

Both sources are merged.


Module Configuration Fields​

FieldTypeRequiredDescription
scriptstringYesPath to the script file. Relative paths are resolved from the project root. Absolute paths and ~/ paths are used as-is.
argsstring[]NoDefault arguments passed to the script
interpreterstringNoScript interpreter (node, python, bash, etc.). If omitted, determined from script extension.
overrideTaskbooleanNoIf true, this module replaces the built-in task with the same name. Default: false.
events.beforestring | string[]NoTask name(s) to hook before. The module runs before these tasks.
events.afterstring | string[]NoTask 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
}
}
warning

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:

GlobalNotes
requireStandard Node.js require
processFull process object
BufferNode.js Buffer
consoleStandard console
setTimeout / clearTimeoutTimers
setInterval / clearIntervalTimers
module.exportsAvailable but not used by builder
Oidis namespacesAll Io.* objects from globalThis
__filename / __dirnameBuilder binary paths (not project paths)
Top-level return is illegal

In 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:

  1. modules field from package.conf.jsonp (normalized relative to project root)
  2. modules field from builder's global commons.jsonp (normalized relative to builder root)
  3. <builder>/resource/configs/modules.register.json
  4. <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"
}
}
}
tip

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