Skip to content

Commit

Permalink
Initial (#1)
Browse files Browse the repository at this point in the history
* init

* fix test

* ci

* wip

* working

* working

* test

* readme

* ci

* update

* upd

* fix

* fix

* tags

* version
  • Loading branch information
hasnainroopawalla authored Aug 25, 2024
1 parent 7741c0b commit a88aae5
Show file tree
Hide file tree
Showing 19 changed files with 3,945 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
dist
55 changes: 55 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"no-magic-numbers": "off",
"no-console": ["warn", { "allow": ["warn", "error"] }],
"no-warning-comments": [
"warn",
{
"terms": ["TODO"],
"location": "start"
}
],
"no-alert": "warn",
"no-duplicate-imports": ["error", { "includeExports": true }],
"no-unused-private-class-members": "error",
"camelcase": "error",
"eqeqeq": "error",
"@typescript-eslint/member-ordering": "error",
"arrow-body-style": ["error", "as-needed"],
"prefer-const": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{
"args": "all",
"argsIgnorePattern": "^_",
"caughtErrors": "all",
"caughtErrorsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"ignoreRestSiblings": true
}
],
"lines-between-class-members": [
"error",
{
"enforce": [
{ "blankLine": "always", "prev": "*", "next": "method" },
{ "blankLine": "always", "prev": "method", "next": "*" },
{ "blankLine": "always", "prev": "field", "next": "field" }
]
},
{ "exceptAfterSingleLine": true }
],
"@typescript-eslint/no-non-null-assertion": "warn",
"no-fallthrough": "error",
"object-shorthand": ["error", "always"]
}
}
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Create tag
on:
push:
branches:
- main

permissions:
contents: write

jobs:
build-createTag:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: "20.x"

- name: Install dependencies
run: yarn setup

- name: Lint
run: yarn lint

- name: Test
run: yarn test

- name: Build
run: yarn build

- name: Create Tag
run: yarn createTag
env:
GH_TOKEN: ${{ secrets.GHUB_TOKEN }}
30 changes: 30 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Build and Test
on:
pull_request:
branches:
- main

jobs:
lint-build-test:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v3

- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: "20.x"

- name: Install dependencies
run: yarn setup

- name: Lint
run: yarn lint

- name: Test
run: yarn test

- name: Build
run: yarn build
32 changes: 32 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Release
on:
release:
types: [published]

jobs:
build-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: "20.x"
registry-url: https://registry.npmjs.org/
- name: Install dependencies
run: yarn setup

- name: Lint
run: yarn lint

- name: Test
run: yarn test

- name: Build
run: yarn build

- name: Publish
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
dist
yarn-error.log
.DS_Store
3 changes: 3 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"arrowParens": "avoid"
}
119 changes: 119 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# ts-mixin

[![Minified Size](https://badgen.net/bundlephobia/min/ts-mixin)](https://bundlephobia.com/result?p=ts-mixin)

## Overview

`ts-mixin` is a lightweight TypeScript Mixin framework. The Mixin pattern is meant to add reusable functionality to objects without any class inheritance ([source](https://www.patterns.dev/vanilla/mixin-pattern/)).

### Features

- Intuitive way to create Mixins for easy integration into existing systems
- Strong type-safety during compilation, along with runtime safety while creating and initializing the Mixins
- Provides inter-class reusability of attributes (props/methods) by passing the _mixed_ object to the initializer (constructor)

## 🏁 Getting started

```
$ npm install ts-mixin
// OR
$ yarn add ts-mixin
```

## 💡 Quick start

A `Bank` system where the core functionality is _mixed_ in from various existing classes like `Amount` and `Deposit`:

```typescript
import { BaseMixin } from "ts-mixin";

type IAmount = {
amount: number;
setAmount: (newAmount: number) => void;
};

// Basic class to manage the amount
class Amount implements IAmount {
public amount: number = 1000;

constructor(private readonly bank: IBank) {}

public setAmount(newAmount: number): void {
this.amount = newAmount;
}
}

// Define the Amount Mixin
class AmountMixin extends BaseMixin<IBank, IAmount> {
constructor() {
super({
// Specify the methods and props to be be mixed
methods: ["setAmount"],
props: ["amount"],
initMixin: bank => new Amount(bank),
});
}
}
```

```typescript
import { BaseMixin } from "ts-mixin";

type IDeposit = {
deposit: (amount: number) => void;
};

// Basic class to deposit money into the account
class Deposit implements IDeposit {
constructor(private readonly bank: IBank) {}

public deposit(depositAmount: number): void {
this.bank.setAmount(this.bank.amount + depositAmount);
}
}

// Define the Deposit Mixin
class DepositMixin extends BaseMixin<IBank, IDeposit> {
constructor() {
super({
// Specify the methods and props to be be mixed
methods: ["deposit"],
props: [],
initMixin: bank => new Deposit(bank),
});
}
}
```

Define the `IBank` interface to represent the Mixed object based on the above Mixins:

```typescript
type IBank = IAmount & IDeposit;
```

Create the mixed `bank` object using an input list of Mixin implementations:

```typescript
import { mix } from "ts-mixin";

const bank = mix<IBank>({
mixins: [new AmountMixin(), new DepositMixin()],
});

// Complete type-safety for the mixed object
console.log(bank.amount); // 1000
bank.deposit(500);
console.log(bank.amount); // 500
bank.setAmount(2000);
console.log(bank.amount); // 2000
```

Runtime safety is guaranteed while mixing objects in the following scenarios:

- Attempting to bind a method as a prop and vice versa
- Attempting to bind a prop/method that already exists on the object

## ✏️ Contributing

- Post any issues and suggestions on the GitHub [issues](https://github.com/hasnainroopawalla/ts-mixin/issues) page.
- To contribute, fork the project and then create a pull request back to `main`.
8 changes: 8 additions & 0 deletions jestconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"transform": {
"^.+\\.(t|j)sx?$": "ts-jest"
},
"testRegex": "\\.test\\.[jt]sx?$",
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"],
"testEnvironment": "jsdom"
}
37 changes: 37 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "ts-mixin",
"version": "0.0.1",
"description": "A lightweight TypeScript Mixin framework.",
"repository": "https://github.com/hasnainroopawalla/ts-mixin",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"setup": "yarn install --ignore-scripts",
"build": "tsc",
"prepare": "yarn build",
"publish:dry-run": "npm publish --dry-run",
"test": "jest --config jestconfig.json",
"test:watch": "jest --config jestconfig.json --watch",
"lint": "eslint \"{**/*,*}.{js,ts,jsx,tsx}\"",
"createTag": "PACKAGE_VERSION=$(cat package.json | grep \\\"version\\\" | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]') && git tag v$PACKAGE_VERSION && git push --tags"
},
"files": [
"dist",
"LICENSE",
"README.md"
],
"keywords": ["typescript", "mixin", "mixins", "oop", "generics", "runtime"],
"author": "Hasnain Roopawalla",
"license": "MIT",
"devDependencies": {
"@types/jest": "^29.5.12",
"@typescript-eslint/eslint-plugin": "^8.2.0",
"@typescript-eslint/parser": "^8.2.0",
"eslint": "^8.48.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"ts-jest": "^29.2.5",
"tslib": "^2.7.0",
"typescript": "^5.5.4"
}
}
22 changes: 22 additions & 0 deletions src/base-mixin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { BindUtils } from "./bind-utils";

type IBaseMixinArgs<TBase, TMixin> = {
props: Array<keyof TMixin>;
methods: Array<keyof TMixin>;
initMixin: (base: TBase) => TMixin;
};

export class BaseMixin<TBase, TMixin> {
private args: IBaseMixinArgs<TBase, TMixin>;

constructor(args: IBaseMixinArgs<TBase, TMixin>) {
this.args = args;
}

public initMixin(base: TBase): void {
const mixin = this.args.initMixin(base);

BindUtils.bindMethods(base, mixin, this.args.methods);
BindUtils.bindProps(base, mixin, this.args.props);
}
}
Loading

0 comments on commit a88aae5

Please sign in to comment.