@itrocks/compose
v0.1.0
Published
Class compositions via configuration file, enabling mixins addition and module exports replacement
Downloads
77
Maintainers
Readme
compose
Class compositions via configuration file, enabling mixins addition and module exports replacement.
Installation
npm install @itrocks/composeUsage
The compose() function must be called as early as possible, before any configured class is loaded.
In practice: call compose() before any import() or require() of application modules.
A minimal and typical setup can be seen in @itrocks/framework.
Example
Given the @itrocks/user package
providing a User class, and an override of this class in a user-override.ts file stored at your project root:
// user-override.ts
import { User } from '@itrocks/user'
export class UserOverride extends User {}Your main entry point may start with:
// main.ts
import { compose } from '@itrocks/compose'
compose(__dirname, {
'@itrocks/user': '/user-override'
})
// ... in code using User through dynamic import/require,
// User will be replaced by UserOverride and include any new or overridden features.Limitations
CommonJS execution
This package relies on dynamic module loading. In Node.js:
require()is dynamic and can be overridden,- native ES
importis static and cannot.
You can write ES import syntax in TypeScript, then transpile it to CommonJS, where imports become require().
Recommended minimal TypeScript configuration:
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"target": "ES2022"
}
}Static import declarations
Static imports must be declared first in modules. As a result, compose() will only affect:
- modules loaded dynamically after it is called,
- static imports declared inside dynamically loaded modules.
API
compose(baseDir, config)
compose(baseDir: string, config: Record<string, string | string[]>): voidParameters:
baseDir: a directory used to resolve paths starting with/config: an object where:- the key is the module (and optional export) to replace
- the value is the replacement module (and optional export), or an array of replacements
Configuration module export format
Both replaced and replacement module / export apply these rules:
- A module path starting with
/refers to the JavaScript file path, relative tobaseDir's argument. - A module path not starting with
/refers to the name of node_modules package or package module. - The name of the export can be explicitly defined after the module path, separated by ':'.
- If no export is given: the default export is used if set, otherwise the first exported value encountered at runtime is used.
- If the
defaultexport is given but the module has no default export, the first exported value encountered at runtime is used.
Note: omitting :exportName is strictly equivalent to using :default.
Single replacement
If one replacement is given for a module, it is applied:
- if the replacement inherits the original type, it is selected as the replacement type,
- otherwise, the original type is kept as the base type, and the replacement is applied as a mixin via @itrocks/use,
Multiple replacements
If multiple replacements are given for a module, they are applied in the order they are defined:
- the first entry that inherits the original type is selected as the replacement base type,
- if no entry inherits the original type: the original type is kept as the base type,
- the remaining entries are applied as mixins via @itrocks/use.
Summary table
| Configuration entry | Effect |
|--------------------------------------------|----------------------------------------------------------------------------------------------------------|
| 'pkg' : 'override' | Replaces the module default export (or first export) with the override default export (or first export). |
| 'pkg:default' : 'override' | Same as above, explicit default on both sides. |
| 'pkg:User' : 'override:UserOverride' | Replaces the named export User with UserOverride. |
| 'pkg' : ['override'] | Single replacement: if it inherits the original type → replacement, otherwise applied as a mixin. |
| 'pkg' : ['override', 'mixinA', 'mixinB'] | First inheriting entry becomes the base type, others are applied as mixins (in order). |
| 'pkg' : ['mixinA', 'mixinB'] | No replacement found: original type kept, all entries applied as mixins. |
| '/local-module' : 'pkg' | Local module (resolved from baseDir) replaces a node_modules package. |
| 'pkg' : '/local-module' | Package export replaced by a local implementation. |
| 'pkg' : 'override:mixinOnly' | No inheritance detected → original kept, override applied as mixin. |
Common mistakes
Calling compose() too late
import { compose } from '@itrocks/compose'
import { User } from '@itrocks/user'
compose(__dirname, { '@itrocks/user': '/user-override' }) // too lateIf a module is already loaded, its exports are fixed.compose() will not retroactively replace anything.
✔️ Always call compose() before loading any module you want to affect.
Using native ES modules at runtime
Running Node.js in native ESM mode means:
importis static- module loading cannot be intercepted
In that case, compose() cannot work.
✔️ Use TypeScript with CommonJS output (even if you write import syntax).
Expecting static imports to be replaced
import { User } from '@itrocks/user'Static imports are resolved before any runtime code runs.
✔️ Only modules loaded:
- dynamically (
require, orimporttranspiled torequireat runtime) - or statically inside dynamically loaded modules
can be affected.
Forgetting that :default is implicit
'@itrocks/user': '/user-override'
This is strictly equivalent to:
'@itrocks/user:default': '/user-override:default'
✔️ If a module has no default export, compose() will use the first exported value it finds.
Assuming an override always replaces the base type
If a replacement does not inherit the original type:
- it is not used as the base
- it is applied as a mixin instead
✔️ To fully replace a type, the replacement must extend or inherit from it.
Mixing up path resolution rules
'@itrocks/user': 'user-override' // node resolution '@itrocks/user': '/user-override' // resolved from baseDir
✔️ Paths starting with / are resolved from baseDir.
✔️ Others are resolved via Node’s module resolution (node_modules).
