Extending the Builder
Oidis Builder can be extended in two ways:
- Module scripts — external scripts hooked into the task lifecycle (prefer the existing Node.js runtime). Introducing another language or runtime requires a justified, approved exception under the technology selection policy.
- Custom tasks — TypeScript classes that integrate fully into the builder as first-class tasks
Option 1: Module Scripts (Recommended for Simple Extensions)
For simple integration, register a thin JavaScript module hook. Put substantial reusable logic in TypeScript and compile/check it through the existing Builder workflow. A small entry point does not require a custom Builder variant or a separate runtime.
See Module System for full details.
// package.conf.jsonp
"modules": {
"my-step": {
"script": "resource/scripts/my-step.js",
"events": { "before": "compile" }
}
}
Option 2: Custom Tasks (Full Integration)
Custom tasks are TypeScript classes that extend BaseTask. They are discovered automatically by the
builder's reflection system — just place them in the correct source directory and run oidis install.
Task Class Structure
// source/typescript/Io/MyProject/Builder/Tasks/MyCustomTask.ts
import { BaseTask } from "@io-oidis-builder/Io/Oidis/Builder/Primitives/BaseTask.js";
export class MyCustomTask extends BaseTask {
protected getName() : string {
return "my-custom-task"; // this becomes the CLI task name
}
protected async processAsync($option? : string) : Promise<void> {
// $option is the part after ":" in "my-custom-task:option"
const version : string = this.project.version;
const src : string = this.properties.sources;
this.fileSystem.Write(
this.properties.projectBase + "/build/my-output.txt",
"Version: " + version + "\nSources: " + src
);
if ($option === "verbose") {
await this.terminal.SpawnAsync("echo", ["Verbose output enabled"]);
}
// Chain other built-in tasks:
await this.runTaskAsync("typescript:source");
}
}
Naming Convention
The task name returned from getName() is what you use on the CLI:
oidis my-custom-task
oidis my-custom-task:verbose
If you do not override getName(), the task name defaults to the class name lowercased without namespace (e.g., MyCustomTask → mycustomtask). Always override getName() to get a readable, hyphenated name.
Available Properties in BaseTask
| Property | Type | Description |
|---|---|---|
this.project | IProject | Full project config from package.conf.jsonp |
this.properties | IProperties | Resolved build paths and capability flags |
this.programArgs | ProgramArgs | Parsed CLI arguments |
this.build | IBuildArgs | Build type, platform, timestamp |
this.builderConfig | IAppConfiguration | Builder's own global configuration |
this.fileSystem | FileSystemHandler | File I/O operations |
this.terminal | Terminal | Shell command execution |
this.externalModules | string | Path to <appdata>/external_modules |
Available Methods in BaseTask
| Method | Description |
|---|---|
runTaskAsync($tasks) | Execute other tasks by name (includes before/after hooks) |
resolveExternConfig($name) | Load a config from resource/configs/<name>.conf.jsonp |
getDependenciesTree($option?) | Override to declare task dependencies (advanced) |
Using resolveExternConfig
Load tool-specific configuration that can be overridden at project level:
protected async processAsync($option? : string) : Promise<void> {
// Loads from: <builder>/resource/configs/my-tool.conf.jsonp
// Overridden by: <project>/bin/project/configs/my-tool.conf.jsonp
// or <project>/resource/configs/my-tool.conf.json
const config : any = await this.resolveExternConfig("my-tool");
const rules : string[] = config.rules;
// ... use config
}
Using terminal.SpawnAsync
Run shell commands from a task:
protected async processAsync() : Promise<void> {
const result = await this.terminal.SpawnAsync(
"my-tool",
["--input", this.properties.sources, "--output", this.properties.dest],
{
cwd: this.properties.projectBase,
advanced: { noExitOnStderr: true }
}
);
if (result.exitCode !== 0) {
// LogIt is from @io-oidis-commons
// do not use console.* — always use LogIt
}
}
Declaring Task Dependencies
Override getDependenciesTree to declare which tasks must run first:
public getDependenciesTree($option? : string) : string[] {
if ($option === "with-compile") {
return ["compile:dev", "my-custom-task"];
}
return ["my-custom-task"];
}
This is used by orchestrator tasks to build the execution chain dynamically.
Registering Tasks in the Builder Itself
If you are contributing to io-oidis-builder CORE:
- Create your task class in
source/typescript/Io/Oidis/Builder/Tasks/<Category>/MyTask.ts - Run
oidis build dev— the build regenerates interface registrations (typescript-interfaces);oidis installdoes not - The task is automatically discovered by
Reflection.getAllClasses()on next startup
No imports or manual registration are needed anywhere.
Adding Project-Specific Tasks
If your task is project-specific (not part of the builder CORE):
- Create the task class in your project's source directory
- The builder loads ALL
Tasks/**/*.{js,mjs}files from its build output at startup - After compilation, your task class is available alongside all built-in tasks
Project-specific task classes must be compiled as part of the builder's source (they need access to @io-oidis-builder imports). This pattern is typically used only when building a custom builder variant.
For project-specific build logic, prefer Module Scripts — they don't require compiling into the builder.
Task Hooks via modules.register.json
For the most common extension need (run something before/after a task), use the file-based hook system:
// <project>/resource/configs/modules.register.json
{
"generate-api": {
"script": "resource/scripts/generate-api.js",
"events": {
"before": "compile"
}
},
"post-package": {
"script": "resource/scripts/post-package.sh",
"interpreter": "bash",
"events": {
"after": "compress"
}
}
}
This file is loaded automatically — no changes to package.conf.jsonp needed.
Logging
Always use LogIt from @io-oidis-commons inside tasks and scripts. Never use console.* directly:
import { LogIt } from "@io-oidis-commons/Io/Oidis/Commons/Utils/LogIt.js";
LogIt.Info("Starting code generation");
LogIt.Warning("Some optional dependency not found");
LogIt.Error("Code generation failed: " + error.message);
LogIt.Debug("Detailed debug info: " + JSON.stringify(data));
In module scripts (Node.js), you can use console.log / console.error — the builder captures stdout/stderr from child processes via its log pipeline.
See also: Tasks Reference | Module System | Builder Overview