UNPKG

@itrocks/compose

Version:

Class compositions via configuration file, enabling mixins addition and module exports replacement

196 lines (135 loc) 7.88 kB
[![npm version](https://img.shields.io/npm/v/@itrocks/compose?logo=npm)](https://www.npmjs.org/package/@itrocks/compose) [![npm downloads](https://img.shields.io/npm/dm/@itrocks/compose)](https://www.npmjs.org/package/@itrocks/compose) [![GitHub](https://img.shields.io/github/last-commit/itrocks-ts/compose?color=2dba4e&label=commit&logo=github)](https://github.com/itrocks-ts/compose) [![issues](https://img.shields.io/github/issues/itrocks-ts/compose)](https://github.com/itrocks-ts/compose/issues) [![discord](https://img.shields.io/discord/1314141024020467782?color=7289da&label=discord&logo=discord&logoColor=white)](https://25.re/ditr) # compose Class compositions via configuration file, enabling mixins addition and module exports replacement. ## Installation ```bash npm install @itrocks/compose ``` ## Usage 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](https://github.com/itrocks-ts/framework/blob/main/src/framework.ts). ## Example Given the [@itrocks/user](https://github.com/itrocks-ts/user/blob/main/src/user.ts) package providing a `User` class, and an override of this class in a `user-override.ts` file stored at your project root: ```ts // user-override.ts import { User } from '@itrocks/user' export class UserOverride extends User {} ``` Your main entry point may start with: ```ts // 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 `import` is static and cannot. You can write ES `import` syntax in TypeScript, then transpile it to CommonJS, where imports become `require()`. **Recommended minimal TypeScript configuration:** ```json { "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) ```ts compose(baseDir: string, config: Record<string, string | string[]>): void ``` **Parameters:** - `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 to `baseDir`'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 `default` export 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](https://github.com/itrocks-ts/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](https://github.com/itrocks-ts/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 ```ts import { compose } from '@itrocks/compose' import { User } from '@itrocks/user' compose(__dirname, { '@itrocks/user': '/user-override' }) // too late ``` If 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: - `import` is 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 ```ts import { User } from '@itrocks/user' ``` Static imports are resolved before any runtime code runs. ✔️ Only modules loaded: - dynamically (`require`, or `import` transpiled to `require` at 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`).