Full Code of LinusBorg/vue-mixable for AI

main 9864d162b0ec cached
40 files
46.0 KB
13.3k tokens
45 symbols
1 requests
Download .txt
Repository: LinusBorg/vue-mixable
Branch: main
Commit: 9864d162b0ec
Files: 40
Total size: 46.0 KB

Directory structure:
gitextract_tjv42v88/

├── .eslintignore
├── .eslintrc.cjs
├── .github/
│   └── workflows/
│       ├── ci.yml
│       └── size.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── .release-it.json
├── .vscode/
│   └── extensions.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── commitlint.config.js
├── index.html
├── package.json
├── src/
│   ├── __tests__/
│   │   ├── advanced.spec.ts
│   │   ├── cache.spec.ts
│   │   ├── computed.spec.ts
│   │   ├── data.spec.ts
│   │   ├── helpers.ts
│   │   ├── inject.spec.ts
│   │   ├── lifecylce.spec.ts
│   │   ├── methods.spec.ts
│   │   ├── propsEmits.spec.ts
│   │   ├── setup.ts
│   │   └── watch.spec.ts
│   ├── createComposable.ts
│   ├── defineMixin.ts
│   ├── env.d.ts
│   ├── index.ts
│   ├── inject.ts
│   ├── typeTest.ts
│   ├── types.ts
│   ├── utils.ts
│   └── vmContextProxy.ts
├── tsconfig.app.json
├── tsconfig.config.json
├── tsconfig.json
├── tsconfig.vitest.json
└── vite.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintignore
================================================
/coverage
/dist

================================================
FILE: .eslintrc.cjs
================================================
/* eslint-env node */
module.exports = {
  root: true,
  extends: ['@linusborg/eslint-config', 'plugin:vue/vue3-essential'],
  parserOptions: {
    ecmaVersion: 'latest',
  },
  overrides: [
    {
      files: ['*.js', '.cjs'],
      env: {
        node: true,
      },
    },
  ],
}


================================================
FILE: .github/workflows/ci.yml
================================================
name: 'ci'
on:
  push:
    branches:
      - '**'
  pull_request:
    branches:
      - main

permissions:
  contents: read # to fetch code (actions/checkout)

jobs:
  build-and-typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Install pnpm
        uses: pnpm/action-setup@v2

      - name: Set node version to 16
        uses: actions/setup-node@v3
        with:
          node-version: 16
          cache: 'pnpm'

      - run: pnpm install

      - name: Run build & tsc
        run: pnpm run build
 
  unit-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Install pnpm
        uses: pnpm/action-setup@v2

      - name: Set node version to 16
        uses: actions/setup-node@v3
        with:
          node-version: 16
          cache: 'pnpm'

      - run: pnpm install

      - name: Run unit tests
        run: pnpm run test:unit
  
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Install pnpm
        uses: pnpm/action-setup@v2

      - name: Set node version to 16
        uses: actions/setup-node@v3
        with:
          node-version: 16
          cache: 'pnpm'

      - run: pnpm install

      - name: Lint codebase
        run: pnpm run lint

================================================
FILE: .github/workflows/size.yml
================================================
name: "size"
on:
  pull_request:
    branches:
      - main
jobs:
  size:
    runs-on: ubuntu-latest
    env:
      CI_JOB_NUMBER: 1
    steps:
      - uses: actions/checkout@v1
      - name: Install pnpm
        uses: pnpm/action-setup@v2
      - uses: andresz1/size-limit-action@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}

================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
.DS_Store
dist
dist-ssr
coverage
*.local

/cypress/videos/
/cypress/screenshots/

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

tsconfig.app.tsbuildinfo

================================================
FILE: .prettierignore
================================================
dist/
/types

================================================
FILE: .prettierrc.json
================================================
{
  "useTabs": false,
  "singleQuote": true,
  "trailingComma": "es5",
  "semi": false
}

================================================
FILE: .release-it.json
================================================
{
  "git": {
    "commitMessage": "chore: release v${version}"
  },
  "github": {
    "release": true
  },
  "plugins": {
    "@release-it/conventional-changelog": {
      "preset": "angular",
      "infile": "CHANGELOG.md"
    }
  },
  "hooks": {
    "before:init": ["pnpm lint:ci", "pnpm test:ci"],
    "after:bump": ["pnpm build"],
    "after:release": ["echo 🥳 Successfully released ${name} v${version}."]
  }
}

================================================
FILE: .vscode/extensions.json
================================================
{
  "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}


================================================
FILE: CHANGELOG.md
================================================


## [0.3.1](https://github.com/LinusBorg/vue-mixable/compare/0.3.0...0.3.1) (2022-11-17)


### Bug Fixes

* **vm:** ensure custom properties can be set and read ([784882f](https://github.com/LinusBorg/vue-mixable/commit/784882febd52ee3c3e07d0ab1b19285c44609b22))

# 0.3.0 (2022-11-07)


### Bug Fixes

* add license field to package.json, add repo URL ([c1acfe6](https://github.com/LinusBorg/vue-mixable/commit/c1acfe6eb2a882d8a893f191d7ea96e644b08392))
* add LICENSE file ([52b59b5](https://github.com/LinusBorg/vue-mixable/commit/52b59b5620f29e9109f2ab461621851a5135424f))
* add proper files option content ([47df2e7](https://github.com/LinusBorg/vue-mixable/commit/47df2e701a1299eef5c4aa4060cfeb69ea757ead))
* ensure cache works with the new types. ([babada7](https://github.com/LinusBorg/vue-mixable/commit/babada7b499e0fa6c3aa6e67402aac1d3857b99d))
* isString should properly guard string type ([1629187](https://github.com/LinusBorg/vue-mixable/commit/16291871c46a7065bf5193f08a89b8c0823a9095))
* make vue peer dep ([c8e73ff](https://github.com/LinusBorg/vue-mixable/commit/c8e73ff0ecebec1e3a1625265b21333e6884635b))
* type in release-it config file name, add missing plugin package. ([7cae268](https://github.com/LinusBorg/vue-mixable/commit/7cae268f7d45fa4fed9b147bb15a6b0c22645d4e))
* **types:** return type should include props and emits keys ([4c38321](https://github.com/LinusBorg/vue-mixable/commit/4c383217a67c54e0b423fdefd96c024fa605ab7a))


### Features

* add composable cache ([0838c4b](https://github.com/LinusBorg/vue-mixable/commit/0838c4b63811422f42c6adfd436d1a2300cadc4c))
* defineMixin() allows to write typesafe mixins. ([1fd5e93](https://github.com/LinusBorg/vue-mixable/commit/1fd5e930f28745218519c01d34215b511458d3a7))
* export defineMixin() ([0b584ec](https://github.com/LinusBorg/vue-mixable/commit/0b584ec12c924d7a04846567cdbf08ef6ffc0ea6))

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2022 - present Thorsten Lünborg

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
![current npm version](https://img.shields.io/npm/v/vue-mixable) ![npm bundle size (min+gzip)](https://badgen.net/bundlephobia/minzip/vue-mixable) ![npm downloads per month](https://img.shields.io/npm/dm/vue-mixable)

![NPM](https://img.shields.io/npm/l/vue-mixable) ![CI checks](https://badgen.net/github/checks/linusborg/vue-mixable) ![types included](https://badgen.net/npm/types/vue-mixable)

# 🌪 `vue-mixable`

> Convert mixins into composables to reuse them in Composition API

* helpful during Options API -> Composition API migrations / in mixed code-bases
* simple API - one function call is all you need
* TS Support (with small caveats)
* small footprint: ![npm bundle size (min+zip)](https://badgen.net/bundlephobia/minzip/vue-mixable)

## Quick Intro

```js
// given an existing mixin such as this:
export const messageMixin  = {
    data() {
        return {
            msg: 'Hello World'
        }
    },
    computed: {
        loudMsg() {
            return this.capitalize(this.msg) + '!!!1eleven!'
        }
    },
    methods: {
        capitalize(value) { return value.toUpperCase() }
    } 
}

// we can create a composable from it with a single function call
import { createComposableFromMixin } from 'vue-mixable'
export const useMessage = createComposableFromMixin(messageMixin)
```

This composable can then be used in  `setup()`/ `<script setup>`:

```html
<script setup>
const {
    msg, // ref
    loudMsg, // computed() ref
    capitalize // function
} = useMessage()
</script>
```

## Use cases

This library is primarily useful for developers trying to migrate a Options-API codebase using Mixins for code sharing to Composition API using composables for code sharing.

One of the challenges in such a migration is that one often cannot rewrite a mixin into a composable and replace all of that mixin's usage instances in the app at once, epsecially when mixins depend on one another, which is often the case in larger code-bases.

This is where `vue-mixable` can help: The team can keep all their mixins for the time of the migration, but convert each of them into composables with one line of code. You get have your cake, and eat it to, in a way. 

Then they can migrate individual components from the mixin to the composable at their own pace, and once the migration is done, they can rewrite the mixin into a proper standalone composable and finally remove the mixin from your codebase.


## Installation

```bash
npm install vue-mixable
```

## Usage Notes

### Supported APIs

`vue-mixable` provides full support for mixins that use the following Options APIs

* `data`
* `computed`
* `methods`
* `watch`
* `provide`
* `inject`
* `props` (see Note in the next section)
* `emits` (see Note in the next section)

Options with no direct support (currently):

* `inheritAttrs`
* `components`
* `directives`
* `name`

If you use any of the above options, you would have to set them manually in any component that uses the generated composable instead of the original mixin.

### `props` & `emits`  options

Mixins can contain props definitions. Composables cannot, as they are functions invoked during component initialization (in `setup()`, at which point props must have been defined already.

`vue-mixable` solves with in the following way:

```js
const mixin = {
    props: ['modelValue', 'age', 'street', 'city'],
    emits: ['modelValue:update', 'certified']
    // ...and other mixin content, i.e.:
    data: () => ({
        //...
    })
}

export const usePerson = createComposableFromMixin(mixin)
// props and emits options will be available 
// as properties on the composable function(!)
usePerson.props // => ['modelValue', 'age', 'street', 'city']
usePerson.emits // => ['modelValue:update', 'certified']
```
Usage
```js
import { usePerson } from '...'

export default defineComponent({
    props: ['firstname', 'lastname', ...usePerson.props],
    emits: usePerson.emits,
    setup(props, { emit }) {
        const person = usePerson()

        return {

        }
    }
})

```
### Shape of the composable's return value

The shape of the return value is essentially a flattened version of the mixins `data`, `computed` and `methods` properties, with `data` and `computed` being `ref()`'s. All other supported properties (lifecylces, `watch`) have nothing to expose externally.

```js
const mixin = {
    data: () =>({
        a: 'A',
        b: 'B',
    }),
    computed: {
        c() { return this.A },
        d() { return this.B }
    },
    methods: {
        e() {
            return callSomething(this.a, this.c)
        }
    }
}

const useComposable = createComposableFromMixin(mixin)
```
would be turned into:
```js

const {
    a, // ref('A')
    b, // ref('B')
    c, // computed ref 
    d, // computed ref
    e, // normal function
} = useComposable()
```

### Feature Roadmap

- [ ] Support Mixins that implicitly depend on properties/APIs from other mixins.
- [ ] Support Nested Mixins.
- [ ] Exclude specific properties from composables return value (essentially making some mixin properties private in the composable).

Out of scope / not planned

- [ ] mixins with implicit circular dependencies on one another.

## Caveats

### `this.$watch()` in created

creating a watcher imperatively in `created` will likely not work as expected, because in the created composable, that hooks is run before `setup()` returns, so any data properties declared in the mixin/composable will be missing on `this`.

Possible workarounds:

- use the normal `watch:`option
- create the watcher in `beforeMount`.

## Typescript Support

Typescript support is still considered unstable as we plan on improving the types, possibly introduction breaking changes to the types.

**Caveats:** 

* For Type inference to work, each mixin object *must* have a `props` key. If your mixin does not include any props, set it to an empty object.
* props always need to be defined in object style. array style is currently not supported and will break type inference.
* the `emits` option cannot be provided in its array form, it must take the more verbose object form.
```ts
const mixin = defineMixin({
    props: {} // needed for proper tyep inference for now,
    emits: {
        'update:modelValue': (v?: any) => true, // this validator can be a NOOP returning `true`
    },
    data: () => ({
        // ...
    })
})

const composable = createCopmposableFromMixin(mixin)
```

### `defineMixin()`

This function does not do anything at runtime, it's just providing tpe inferrence for your mixins:

```ts
const mixin = {
    data: () => ({
        msg: 'Hello World',
    }),
    methods: {
        test() {
            this.msg // not inferreed correctly
        }
    }
}

// better:
import { defineMixin } from 'vue-mixable'
const mixin = defineMixin({
    props: {}, // needed, see caveat explained further up.
    data: () => ({
        msg: 'Hello World',
    }),
    methods: {
        test() {
            this.msg // properly inferred.
        }
    }
}
```

### `createComposableFromMixin()`

This function will offer full type inference for any mixin passed to it.


## Developer Instructions


### Compile and Minify for Production, create Type Declarations

```sh
pnpm build
```

### Run Unit Tests with [Vitest](https://vitest.dev/)

```sh
pnpm test:unit
```

### Lint with [ESLint](https://eslint.org/)

```sh
pnpm lint
```


================================================
FILE: commitlint.config.js
================================================
const config = require('@commitlint/config-angular')
module.exports = {
  extends: ['@commitlint/config-angular'],
  rules: {
    'type-enum': [2, 'always', [...config.rules['type-enum'][2], 'chore']],
  },
}


================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>


================================================
FILE: package.json
================================================
{
  "name": "vue-mixable",
  "description": "Turn Vue Mixins into Composables with a simple helper function",
  "version": "0.3.1",
  "license": "MIT",
  "keywords": [
    "Vue",
    "composables",
    "mixins",
    "Vue plugin",
    "migration",
    "compat"
  ],
  "author": {
    "name": "Thorsten Lünborg",
    "url": "https://github.com/linusborg"
  },
  "repository": {
    "url": "https://github.com/LinusBorg/vue-mixable",
    "type": "git"
  },
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "unpkg": "./dist/index.js",
  "jsdelivr": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "require": "./dist/index.cjs",
      "import": "./dist/index.mjs",
      "default": "./dist/index.cjs"
    },
    "./package.json": "./package.json"
  },
  "files": [
    "dist",
    "README.md",
    "LICENSE",
    "src"
  ],
  "scripts": {
    "dev": "vite",
    "build": "vite build && tsc --declaration --emitDeclarationOnly -p tsconfig.app.json --outDir dist",
    "type-check": "tsc --noEmit -p tsconfig.vitest.json --composite false",
    "test:unit": "vitest --environment jsdom",
    "test:ci": "vitest --environment jsdom --run",
    "lint:ci": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts",
    "lint": "pnpm lint:ci --fix",
    "size": "pnpm size-limit",
    "release": "release-it",
    "update-hooks": "pnpm simple-git-hooks"
  },
  "packageManager": "pnpm@7.14.1",
  "simple-git-hooks": {
    "pre-commit": "pnpm lint-staged",
    "commit-msg": "pnpm commitlint --edit $1"
  },
  "lint-staged": {
    "*.{ts,js,cjs,vue}": "pnpm exec eslint --max-warnings 0"
  },
  "size-limit": [
    {
      "path": "dist/index.cjs",
      "limit": "2kB"
    }
  ],
  "devDependencies": {
    "@commitlint/cli": "^17.2.0",
    "@commitlint/config-angular": "^17.2.0",
    "@linusborg/eslint-config": "^0.3.0",
    "@release-it/conventional-changelog": "^5.1.1",
    "@size-limit/preset-small-lib": "^8.1.0",
    "@types/jsdom": "^20.0.1",
    "@types/node": "^16.18.3",
    "@vitest/coverage-c8": "^0.25.0",
    "@vue/test-utils": "^2.1.0",
    "@vue/tsconfig": "^0.1.3",
    "eslint": "^8.27.0",
    "jsdom": "^20.0.2",
    "lint-staged": "^13.0.3",
    "prettier": "^2.7.1",
    "release-it": "^15.5.0",
    "simple-git-hooks": "^2.8.1",
    "size-limit": "^8.1.0",
    "type-fest": "^3.2.0",
    "typescript": "~4.7.4",
    "vite": "^3.2.3",
    "vitest": "^0.25.0",
    "vue": "^3.2.41"
  },
  "peerDependencies": {
    "vue": "^3.2"
  },
  "peerDependenciesMeta": {
    "vue": {
      "optional": true
    }
  }
}


================================================
FILE: src/__tests__/advanced.spec.ts
================================================
import { describe, test, expect, vi } from 'vitest'
import { createComposableFromMixin } from '../createComposable'
import { defineMixin } from '../defineMixin'
import { wrapComposable } from './helpers'

// FIXME: needs to wait until feature is done.
describe.skip('advanced', () => {
  test('nested mixins', async () => {
    const innerSpy = vi.fn()
    const outerSpy = vi.fn()
    const innerMixin = defineMixin({
      props: {
        inner: Number,
      },
      data: () => ({
        nestedProperty: 'A',
        sharedProperty: 'A',
      }),
      created() {
        innerSpy()
      },
      mounted() {
        innerSpy()
      },
    })
    const outerMixin = defineMixin({
      mixins: [innerMixin],
      props: {
        outer: String,
      },
      data: () => ({
        msg: 'Hello World',
        sharedProperty: 'B',
      }),
      computed: {
        getNestedProperty(): string {
          return this.nestedProperty
        },
      },
      mounted() {
        outerSpy()
      },
    })
    // eslint-disable-next-line no-autofix/unused-imports/no-unused-vars
    const innerComposable = createComposableFromMixin(innerMixin)

    const outerComposable = createComposableFromMixin(outerMixin)
    const wrapper = wrapComposable(
      outerComposable,
      {
        props: {
          outer: 'Hello',
          inner: 10,
        },
      },
      {
        props: outerComposable.props,
      }
    )
    // Props
    expect(wrapper.vm.outer).toBe('Hello')
    expect(wrapper.vm.inner).toBe(10)
    // Data & computed
    expect(wrapper.vm.sharedProperty).toBe('B')
    expect(wrapper.vm.getNestedProperty).toBe('A')
    // Lifecycle Hooks
    expect(innerSpy).toHaveBeenCalledTimes(2)
    expect(outerSpy).toHaveBeenCalledTimes(1)
  })
})


================================================
FILE: src/__tests__/cache.spec.ts
================================================
import { describe, test, expect } from 'vitest'
import { createComposableFromMixin } from '../createComposable'
import { defineMixin } from '../defineMixin'

describe('cache', () => {
  test('works', () => {
    const mixin = defineMixin({
      props: {},
      data: () => ({
        msg: 'Hello World',
      }),
    })

    const composable1 = createComposableFromMixin(mixin)
    const composable2 = createComposableFromMixin(mixin)

    expect(composable1).toBe(composable2)
  })
})


================================================
FILE: src/__tests__/computed.spec.ts
================================================
import { describe, test, expect } from 'vitest'
import { createComposableFromMixin } from '../createComposable'
import { defineMixin } from '../defineMixin'
import { wrapComposable } from './helpers'

describe('computed option', async () => {
  test('computed are readable', async () => {
    const mixin = defineMixin({
      props: {},
      computed: {
        msg() {
          return 'msg'
        },
      },
    } as const)

    const composable = createComposableFromMixin(mixin)
    const wrapper = wrapComposable(composable)

    expect((wrapper.vm as any).msg).toBe('msg')
  })

  test('writeable computeds work', async () => {
    const mixin = defineMixin({
      props: {},
      data: () => ({
        internalMsg: 'A',
      }),
      computed: {
        msg: {
          get(): string {
            return this.internalMsg
          },
          set(v: string) {
            this.internalMsg = v
          },
        },
      },
    } as const)

    const composable = createComposableFromMixin(mixin)
    const wrapper = wrapComposable(
      composable,
      {},
      {
        template: `<div>{{ msg }}</div><button @click="msg = 'B'">Click</button>`,
      }
    )

    expect(wrapper.find('div').text()).toBe('A')
    await wrapper.find('button').trigger('click')
    expect(wrapper.find('div').text()).toBe('B')
  })

  test('computed have access to `this`', async () => {
    const mixin = defineMixin({
      props: {},
      computed: {
        msg() {
          return !!this.$emit
        },
      },
    } as const)

    const composable = createComposableFromMixin(mixin)
    const wrapper = wrapComposable(composable)

    expect((wrapper.vm as any).msg).toBe(true)
  })

  test('computed have access to data from mixin', async () => {
    const mixin = defineMixin({
      props: {},
      data: () => ({
        foo: 'Hello World',
      }),
      computed: {
        msg(): string {
          return this.foo
        },
      },
    } as const)

    const composable = createComposableFromMixin(mixin)
    const wrapper = wrapComposable(composable)

    expect((wrapper.vm as any).msg).toBe('Hello World')
  })
})


================================================
FILE: src/__tests__/data.spec.ts
================================================
import { describe, test, expect } from 'vitest'
import { nextTick } from 'vue'
import { createComposableFromMixin } from '../createComposable'
import { defineMixin } from '../defineMixin'
import { wrapComposable } from './helpers'

describe('data Option', () => {
  test('data usable in template', async () => {
    const mixin = defineMixin({
      props: {},
      data: () => ({
        msg: 'Hello World',
      }),
    } as const)

    const composable = createComposableFromMixin(mixin)
    // const wrapper =
    const wrapper = wrapComposable(composable)

    expect(wrapper.text()).toBe('Hello World')
  })
  test('data fn receives instance proxy', async () => {
    const mixin = defineMixin({
      props: {},
      data: (vm: any) => {
        return {
          msg: vm.$emit ? 'Hello World' : '',
        }
      },
    } as const)

    const composable = createComposableFromMixin(mixin)
    // const wrapper =
    const wrapper = wrapComposable(composable, {}, { withProxy: true })

    expect(wrapper.text()).toBe('Hello World')
  })
  test('data properties can be reactively reassigned', async () => {
    const mixin = defineMixin({
      props: {},
      data: () => {
        return {
          msg: 'A',
        }
      },
    } as const)

    const composable = createComposableFromMixin(mixin)
    const wrapper = wrapComposable(
      composable,
      {},
      {
        template: `<div>{{ msg }}</div><button @click="msg = 'B'">Click</button>`,
      }
    )

    expect(wrapper.find('div').text()).toBe('A')

    await wrapper.find('button').trigger('click')
    await nextTick()
    expect(wrapper.find('div').text()).toBe('B')
  })
})


================================================
FILE: src/__tests__/helpers.ts
================================================
import { mount, type MountingOptions } from '@vue/test-utils'
import { defineComponent } from 'vue'

export const wrapComposable = <O extends MountingOptions<{}>>(
  composable: any,
  options: O = {} as O,
  extensions: Record<string, any> = {}
) =>
  mount(
    defineComponent({
      ...extensions,
      template: extensions.template ?? '<div>{{ msg }}</div>',
      setup() {
        return {
          ...composable(),
        }
      },
    }),
    options
  )


================================================
FILE: src/__tests__/inject.spec.ts
================================================
import { describe, test, expect } from 'vitest'
import { defineComponent, ref } from 'vue'
import { createComposableFromMixin } from '../createComposable'
import { defineMixin } from '../defineMixin'
import { wrapComposable } from './helpers'

describe('inject option', async () => {
  test('injects from mixin work', async () => {
    const mixin = defineMixin({
      props: {},
      inject: {
        foo: {
          from: 'foo',
          default: 'bar',
        },
      },
      data(): { msg: string } {
        return {
          msg: (this as any).foo,
        }
      },
    })

    const composable = createComposableFromMixin(mixin)
    const wrapper = wrapComposable(
      composable,
      {},
      {
        template: `<div>{{ msg }}</div>`,
      }
    )

    expect(wrapper.vm.msg).toBe('bar')
  })

  test('provide works', async () => {
    const foo = ref('foo')
    const mixin = defineMixin({
      props: {},
      provide: {
        foo,
      },
    })
    const Child = defineComponent({
      inject: ['foo'],
      template: `<div @click="foo = 'bar'">{{ foo }}</div>`,
    })
    const composable = createComposableFromMixin(mixin)
    const wrapper = wrapComposable(
      composable,
      {},
      {
        template: `<div><Child /></div>`,
        components: {
          Child,
        },
      }
    )

    const child = wrapper.findComponent(Child)
    expect(child.text()).toBe('foo')

    await child.find('div').trigger('click')
    expect(foo.value).toBe('bar')
    expect(child.text()).toBe('bar')
  })
})


================================================
FILE: src/__tests__/lifecylce.spec.ts
================================================
import { describe, test, expect, vi } from 'vitest'
import { createComposableFromMixin } from '../createComposable'
import { defineMixin } from '../defineMixin'
import { wrapComposable } from './helpers'

describe('Lifecycle hooks', async () => {
  test('are called with proper `this`', async () => {
    const spy = vi.fn()
    function testHandler() {
      // @ts-expect-error - doesn't like this for some reason
      if ((this as any).$emit) spy()
    }
    const mixin = defineMixin({
      props: {},
      data: () => ({
        msg: 'A',
      }),
      beforeCreate: testHandler,
      created: testHandler,
      beforeMount: testHandler,
      mounted: testHandler,
      beforeUpdate: testHandler,
      updated: testHandler,
      beforeUnmount: testHandler,
      unmounted: testHandler,
    })

    const composable = createComposableFromMixin(mixin)
    const wrapper = wrapComposable(
      composable,
      {},
      {
        template: `<div>{{ msg }}</div><button @click="msg = 'B'">Click</button>`,
      }
    )

    expect(spy).toHaveBeenCalledTimes(4)

    await wrapper.find('button').trigger('click')

    expect(spy).toHaveBeenCalledTimes(6)

    await wrapper.unmount()

    expect(spy).toHaveBeenCalledTimes(8)
  })

  test('can read and set custom properties on `this`', async () => {
    const mixin = defineMixin({
      props: {},
      beforeCreate() {
        ;(this as any).custom = 'A'
      },
      created() {
        expect((this as any).custom).toBe('A')
      },
    })

    const composable = createComposableFromMixin(mixin)

    const wrapper = wrapComposable(
      composable,
      {},
      {
        template: `<div/>`,
      }
    )

    expect((wrapper.vm as any).custom).toBe('A')
  })
})


================================================
FILE: src/__tests__/methods.spec.ts
================================================
import { describe, test, expect } from 'vitest'
import type { ComponentPublicInstance } from 'vue'
import { createComposableFromMixin } from '../createComposable'
import { defineMixin } from '../defineMixin'
import { wrapComposable } from './helpers'

describe('methods option', () => {
  test('methods are callable', async () => {
    const mixin = defineMixin({
      props: {},
      methods: {
        msg() {
          return 'msg'
        },
      },
    })

    const composable = createComposableFromMixin(mixin)
    const wrapper = wrapComposable(composable)

    expect((wrapper.vm as any).msg()).toBe('msg')
  })

  test('methods have access to `this`', async () => {
    const mixin = defineMixin({
      props: {},
      methods: {
        msg() {
          return !!(this as unknown as ComponentPublicInstance).$emit
        },
      },
    })

    const composable = createComposableFromMixin(mixin)
    const wrapper = wrapComposable(composable)

    expect((wrapper.vm as any).msg()).toBe(true)
  })

  test('methods have access to data from mixin', async () => {
    const mixin = defineMixin({
      props: {},
      data: () => ({
        msg: 'Hello World',
      }),
      methods: {
        getMsg() {
          return (this as any).msg
        },
      },
    })

    const composable = createComposableFromMixin(mixin)
    const wrapper = wrapComposable(composable)

    expect((wrapper.vm as any).getMsg()).toBe('Hello World')
  })
})


================================================
FILE: src/__tests__/propsEmits.spec.ts
================================================
import { describe, test, expect, vi } from 'vitest'
import { defineComponent } from 'vue'
import { mount } from '@vue/test-utils'
import { createComposableFromMixin } from '../createComposable'
import { defineMixin } from '../defineMixin'
// import { wrapComposable } from './helpers'

describe('props', () => {
  test('props as property on composable', async () => {
    const mixin = defineMixin({
      props: {
        foo: String,
      },
    })

    const composable = createComposableFromMixin(mixin)
    const Comp = defineComponent({
      props: composable.props,
      template: `<div>{{ foo }}</div>`,
    })

    const wrapper = await mount(Comp, {
      props: {
        foo: 'bar',
      },
    })

    expect(wrapper.find('div').text()).toBe('bar')
  })
})

describe('emits', () => {
  test('emits option on composable', async () => {
    const mixin = defineMixin({
      props: {},
      emits: ['foo'],
    })

    const composable = createComposableFromMixin(mixin)
    const Comp = defineComponent({
      emits: composable.emits,
      template: `<button @click="$emit('foo')">Click me</button>`,
    })
    const spy = vi.fn()
    const wrapper = await mount(Comp, {
      props: {
        onFoo: spy,
      },
    })

    await wrapper.find('button').trigger('click')
    expect(spy).toHaveBeenCalledTimes(1)
    expect(wrapper.emitted()).toHaveProperty('foo')
  })
})


================================================
FILE: src/__tests__/setup.ts
================================================
import { config } from '@vue/test-utils'

config.global.config.unwrapInjectedRef = true


================================================
FILE: src/__tests__/watch.spec.ts
================================================
import { describe, test, expect, vi } from 'vitest'
import { nextTick } from 'vue'
import { createComposableFromMixin } from '../createComposable'
import { defineMixin } from '../defineMixin'
import { wrapComposable } from './helpers'

describe('watch option', async () => {
  test('string watch', async () => {
    const spy = vi.fn()
    const mixin = defineMixin({
      props: {},
      data: () => ({
        msg: 'A',
      }),
      watch: {
        msg(...args: any[]) {
          spy(...args)
        },
      },
    })

    const composable = createComposableFromMixin(mixin)
    const wrapper = wrapComposable(
      composable,
      {},
      {
        template: `<div>{{ msg }}</div><button @click="msg = 'B'">Click</button>`,
      }
    )

    expect(spy).toHaveBeenCalledTimes(0)

    await wrapper.find('button').trigger('click')
    await nextTick()
    expect(wrapper.vm.msg).toBe('B')
    expect(spy).toHaveBeenCalledTimes(1)
    expect(spy).toHaveBeenCalledWith('B', 'A', expect.any(Function))
  })
})


================================================
FILE: src/createComposable.ts
================================================
/**
 * Code in this file is based on the Options API implementation in Vue 3's codebase:
 * https://github.com/vuejs/core/blob/f67bb500b6071bc0e55a89709a495a27da73badd/packages/runtime-core/src/componentOptions.ts
 */
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onActivated,
  onDeactivated,
  onBeforeUnmount,
  onUnmounted,
  onRenderTracked,
  onRenderTriggered,
  onErrorCaptured,
  getCurrentInstance,
  ref,
  computed,
  watch,
  reactive,
  provide,
  type ComponentPublicInstance,
  type WatchCallback,
} from 'vue'
import { callHook, isArray, isFunction, isObject, isString } from './utils'
import { createContextProxy } from './vmContextProxy'

import type {
  ComputedOptions,
  MethodOptions,
  ComponentOptionsWithObjectProps,
  ComponentOptionsMixin,
  ToRefs,
  CreateComponentPublicInstance,
  ExtractPropTypes,
  ComponentPropsOptions,
  ExtractDefaultPropTypes,
  EmitsOptions,
} from 'vue'

// import { cache } from './cache'

import type {
  ComponentWatchOptionItem,
  ExtractComputedReturns,
  EmitsToProps,
} from './types'
import { resolveInjections } from './inject'

export const cache = new Map<any, any>() // TODO: properly type this

export /* @__PURE__ */ function createComposableFromMixin<
  Props extends Readonly<ExtractPropTypes<PropsOptions>> & EmitsToProps<E>,
  VM extends CreateComponentPublicInstance<
    Props,
    RawBindings,
    D,
    C,
    M,
    Mixin,
    Extends,
    E,
    Props,
    ExtractDefaultPropTypes<PropsOptions>,
    false
  >,
  PropsOptions extends Readonly<ComponentPropsOptions>,
  RawBindings,
  D,
  C extends ComputedOptions = {},
  M extends MethodOptions = {},
  Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
  E extends EmitsOptions = {},
  EE extends string = string
>(
  mixin: ComponentOptionsWithObjectProps<
    PropsOptions,
    RawBindings,
    D,
    C,
    M,
    Mixin,
    Extends,
    E,
    EE
  >
): (() => ToRefs<D> & ToRefs<ExtractComputedReturns<C>> & M) & {
  props: PropsOptions
  emits: E
} {
  const {
    props,
    emits,

    data: dataFn,
    computed: computedOptions,
    methods,
    watch,

    provide: provideOptions,
    inject: injectOptions,

    // Lifecylce Hooks
    created,
    beforeCreate,
    beforeMount,
    mounted,
    beforeUpdate,
    updated,
    activated,
    deactivated,
    beforeUnmount,
    unmounted,
    renderTracked,
    renderTriggered,
    errorCaptured,

    // mixins,
  } = mixin

  if (cache.has(mixin)) {
    return cache.get(mixin) as any
  }

  const composable = () => {
    const instance = getCurrentInstance()!
    const vm = instance.proxy! as VM

    // if (mixins) {
    //   mixins.forEach((mixin) => {
    //     const composable = cache.get(mixin) ?? createComposableFromMixin(mixin)

    //     Object.assign(context, composable())
    //   })
    // }

    const context = {} as ToRefs<D> & ToRefs<ExtractComputedReturns<C>> & M
    const reactiveContext = reactive(context)
    const vmContextProxy = createContextProxy(vm, reactiveContext) as VM

    beforeCreate && callHook(beforeCreate, instance, vm, 'bc')

    // methods
    if (methods) {
      for (const key in methods) {
        context[key] = methods[key].bind(vmContextProxy as any)
      }
    }

    //inject
    if (injectOptions) {
      resolveInjections(injectOptions, context)
    }

    // data
    if (dataFn) {
      // FIXME - any has to go here
      const data = dataFn.call(vmContextProxy as any, vmContextProxy as any)
      for (const key in data) {
        // FIXME - TS `this`error
        // @ts-expect-error this doesn't quite match yet
        context[key] = ref(data[key])
      }
    }

    // computed
    if (computedOptions) {
      for (const key in computedOptions) {
        const def = computedOptions[key]
        const c = isFunction(def)
          ? computed(def.bind(vmContextProxy as any))
          : computed({
              get: def.get.bind(vmContextProxy),
              set: def.set.bind(vmContextProxy),
            })
        // FIXME - TS `this`error
        // @ts-expect-error this doesn't quite match yet
        context[key] = c
      }
    }

    //watch
    if (watch) {
      for (const key in watch) {
        const def = watch[key]
        if (isArray(def)) {
          def.forEach((d) => createWatcher(d, vmContextProxy, key))
        } else {
          createWatcher(def, vmContextProxy, key)
        }
      }
    }

    // provide
    if (provideOptions) {
      const provides = isFunction(provideOptions)
        ? provideOptions.call(vmContextProxy)
        : provideOptions
      Reflect.ownKeys(provides).forEach((key) => {
        provide(key, provides[key])
      })
    }

    // Lifecycle
    created && callHook(created, instance, vm, 'c')
    beforeMount && onBeforeMount(beforeMount.bind(vm))
    mounted && onMounted(mounted.bind(vm))
    beforeUpdate && onBeforeUpdate(beforeUpdate.bind(vm))
    updated && onUpdated(updated.bind(vm))
    activated && onActivated(activated.bind(vm))
    deactivated && onDeactivated(deactivated.bind(vm))
    beforeUnmount && onBeforeUnmount(beforeUnmount.bind(vm))
    unmounted && onUnmounted(unmounted.bind(vm))
    renderTracked && onRenderTracked(renderTracked.bind(vm))
    renderTriggered && onRenderTriggered(renderTriggered.bind(vm))
    errorCaptured && onErrorCaptured(errorCaptured.bind(vm))

    return context
  }

  Object.assign(composable, {
    props,
    emits,
  })

  cache.set(mixin, composable)
  return composable as typeof composable & { props: PropsOptions; emits: E }
}

function createWatcher(
  raw: ComponentWatchOptionItem,
  // ctx: Record<string, any>,
  publicThis: ComponentPublicInstance & Record<string, any>,
  key: string
) {
  const getter = key.includes('.')
    ? createPathGetter(publicThis, key)
    : () => (publicThis as any)[key]
  if (isString(raw)) {
    const handler = publicThis[raw]
    if (isFunction(handler)) {
      watch(getter, handler as WatchCallback)
    }
  } else if (isFunction(raw)) {
    watch(getter, raw.bind(publicThis))
  } else if (isObject(raw)) {
    if (isArray(raw)) {
      raw.forEach((r) => createWatcher(r, publicThis, key))
    } else {
      const handler = isFunction(raw.handler)
        ? raw.handler.bind(publicThis)
        : (publicThis[raw.handler] as WatchCallback)
      if (isFunction(handler)) {
        watch(getter, handler, raw)
      }
    }
  }
}

function createPathGetter(ctx: any, path: string) {
  const segments = path.split('.')
  return () => {
    let cur = ctx
    for (let i = 0; i < segments.length && cur; i++) {
      cur = cur[segments[i]]
    }
    return cur
  }
}


================================================
FILE: src/defineMixin.ts
================================================
import type {
  ComputedOptions,
  MethodOptions,
  ComponentOptionsWithObjectProps,
  ComponentOptionsMixin,
  ComponentPropsOptions,
  EmitsOptions,
} from 'vue'

export /** #__PURE__*/ function defineMixin<
  PropsOptions extends Readonly<ComponentPropsOptions>,
  RawBindings,
  D,
  C extends ComputedOptions = {},
  M extends MethodOptions = {},
  Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
  E extends EmitsOptions = {},
  EE extends string = string
>(
  options: ComponentOptionsWithObjectProps<
    PropsOptions,
    RawBindings,
    D,
    C,
    M,
    Mixin,
    Extends,
    E,
    EE
  >
): typeof options {
  return options
}


================================================
FILE: src/env.d.ts
================================================
/// <reference types="vite/client" />


================================================
FILE: src/index.ts
================================================
export { createComposableFromMixin } from './createComposable'
export { defineMixin } from './defineMixin'


================================================
FILE: src/inject.ts
================================================
/**
 * @legal
 * This code is largely based on code from the Vue 3 codebase:
 * https://github.com/vuejs/core/blob/f67bb500b6071bc0e55a89709a495a27da73badd/packages/runtime-core/src/componentOptions.ts#L823-L876
 * https://github.com/vuejs/core/blob/f67bb500b6071bc0e55a89709a495a27da73badd/packages/runtime-core/src/componentOptions.ts#L1073-L1084
 */

import {
  inject,
  isRef,
  type ComponentPublicInstance,
  type DebuggerEvent,
  type Ref,
} from 'vue'

export type ComponentInjectOptions = string[] | ObjectInjectOptions

export type ObjectInjectOptions = Record<
  string | symbol,
  string | symbol | { from?: string | symbol; default?: unknown }
>
export type DebuggerHook = (e: DebuggerEvent) => void
export type ErrorCapturedHook<TError = unknown> = (
  err: TError,
  instance: ComponentPublicInstance | null,
  info: string
) => boolean | void

import { isArray, isObject } from './utils'

export function resolveInjections(
  injectOptions: ComponentInjectOptions,
  ctx: any
) {
  if (isArray(injectOptions)) {
    injectOptions = normalizeInject(injectOptions)!
  }
  for (const key in injectOptions) {
    const opt = (injectOptions as ObjectInjectOptions)[key]
    let injected: unknown
    if (isObject(opt)) {
      if ('default' in opt) {
        injected = inject(
          opt.from || key,
          opt.default,
          true /* treat default function as factory */
        )
      } else {
        injected = inject(opt.from || key)
      }
    } else {
      injected = inject(opt)
    }
    if (isRef(injected)) {
      // TODO remove the check in 3.3
      Object.defineProperty(ctx, key, {
        enumerable: true,
        configurable: true,
        get: () => (injected as Ref).value,
        set: (v) => ((injected as Ref).value = v),
      })
    } else {
      ctx[key] = injected
    }
  }
}

function normalizeInject(
  raw: ComponentInjectOptions | undefined
): ObjectInjectOptions | undefined {
  if (!isArray(raw)) return raw

  const res: ObjectInjectOptions = {}
  for (let i = 0; i < raw.length; i++) {
    res[raw[i]] = raw[i]
  }
  return res
}


================================================
FILE: src/typeTest.ts
================================================
/**
 * This file is not to be used anywhere, it's just to test the type inference
 */
import { defineComponent } from 'vue'
import { createComposableFromMixin } from './createComposable'
import { defineMixin } from './defineMixin'

interface User {
  name: string
  street: string
  city: string
  hobbies: string[]
}

const mixin = defineMixin({
  props: {
    show: { type: Boolean },
    msg: { type: String, required: true },
  },
  emits: {
    // eslint-disable-next-line no-autofix/unused-imports/no-unused-vars
    'update:show': (value?: any) => true,
  },
  data: () => ({
    msg: 'Hello World',
    age: 10,
    user: {} as User,
  }),
  computed: {
    birthYear(): number {
      return new Date().getFullYear() - this.age
    },
  },
  methods: {
    testFn(): void {
      console.log(this.user)
    },
  },
} as const)

const testComposable = createComposableFromMixin(mixin)

/* eslint-disable no-autofix/unused-imports/no-unused-vars */
const Comp = defineComponent({
  setup() {
    const state = testComposable()

    state.age
    state.msg
    state.user.value
    state.birthYear
    state.testFn()
  },
})

const props = testComposable.props
const emits = testComposable.emits

const CompWMixin = defineComponent({
  mixins: [mixin],
})

type TCompWMixin = InstanceType<typeof CompWMixin>
type checkAge = TCompWMixin['age']
type checkUser = TCompWMixin['user']


================================================
FILE: src/types.ts
================================================
/**
 * @legal
 * Various types in this file are based on code from the Vue 3 codebase:
 * https://github.com/vuejs/core/blob/f67bb500b6071bc0e55a89709a495a27da73badd/packages/runtime-core/src/componentOptions.ts
 */

// TODO: replace with ComponentOptions or something
import type {
  ComponentPublicInstance,
  ComputedGetter,
  EmitsOptions,
  ObjectEmitsOptions,
  WatchCallback,
  WatchOptions,
  WritableComputedOptions,
} from 'vue'

export type EmitsToProps<T extends EmitsOptions> = T extends string[]
  ? {
      [K in string & `on${Capitalize<T[number]>}`]?: (...args: any[]) => any
    }
  : T extends ObjectEmitsOptions
  ? {
      [K in string &
        `on${Capitalize<string & keyof T>}`]?: K extends `on${infer C}`
        ? T[Uncapitalize<C>] extends null
          ? (...args: any[]) => any
          : (
              ...args: T[Uncapitalize<C>] extends (...args: infer P) => any
                ? P
                : never
            ) => any
        : never
    }
  : {}

export interface MethodOptions<Context = ComponentPublicInstance> {
  [key: string]: (
    this: ComponentPublicInstance & Context,
    ...args: any[]
  ) => any
}

export type ComputedOptions = Record<
  string,
  ComputedGetter<any> | WritableComputedOptions<any>
>
export type ContextualizedComputedOptions<Context, T = any> = Record<
  string,
  | ((this: ComponentPublicInstance & Context, ...args: any[]) => any)
  | {
      get(): T
      set(v: T): void
    }
>
export type ExtractComputedReturns<T extends any> = {
  [key in keyof T]: T[key] extends { get: (...args: any[]) => infer TReturn }
    ? TReturn
    : T[key] extends (...args: any[]) => infer TReturn
    ? TReturn
    : never
}

export type ObjectWatchOptionItem = {
  handler: WatchCallback | string
} & WatchOptions
type WatchOptionItem = string | WatchCallback | ObjectWatchOptionItem
export type ComponentWatchOptionItem = WatchOptionItem | WatchOptionItem[]


================================================
FILE: src/utils.ts
================================================
import {
  callWithAsyncErrorHandling,
  type ComponentInternalInstance,
  type ComponentPublicInstance,
} from 'vue'

export const isFunction = (val: unknown): val is Function =>
  typeof val === 'function'
export const isArray = Array.isArray
export const isObject = (val: unknown): val is Record<any, any> =>
  val !== null && typeof val === 'object'
export const isString = (val: unknown): val is string => typeof val === 'string'

/**
 * @legal
 * taken from the Vue 3 Codebase, slightly adjusted:
 * https://github.com/vuejs/core/blob/f67bb500b6071bc0e55a89709a495a27da73badd/packages/runtime-core/src/componentOptions.ts#L878-L890
 */
export function callHook(
  hook: Function,
  instance: ComponentInternalInstance,
  vm: ComponentPublicInstance,
  type: 'c' | 'bc'
) {
  callWithAsyncErrorHandling(
    isArray(hook) ? hook.map((h) => h.bind(vm)) : hook.bind(vm),
    instance,
    type as any
  )
}


================================================
FILE: src/vmContextProxy.ts
================================================
import type { ComponentPublicInstance } from 'vue'

export /* #__PURE__ */ function createContextProxy(
  _vm: ComponentPublicInstance,
  context: Record<string, any>
) {
  return new Proxy(_vm, {
    get(vm, key, receiver) {
      if (key in context) {
        return Reflect.get(context, key, receiver)
      } else {
        return (vm as any)[key]
      }
    },
    set(vm, key, value, receiver) {
      if (key in context) {
        return Reflect.set(context, key, value, receiver)
      } else {
        return Reflect.set(vm, key, value)
      }
    },
    has(vm, property) {
      return Reflect.has(context, property) || Reflect.has(vm, property)
    },
    getOwnPropertyDescriptor(vm, property) {
      if (property in context) {
        return Reflect.getOwnPropertyDescriptor(context, property)
      } else {
        return Reflect.getOwnPropertyDescriptor(vm, property)
      }
    },
    defineProperty(vm, property, descriptor) {
      return Reflect.defineProperty(vm, property, descriptor)
    },
    deleteProperty(vm, property) {
      if (property in context) {
        return Reflect.deleteProperty(context, property)
      } else {
        return Reflect.deleteProperty(vm, property)
      }
    },
  })
}


================================================
FILE: tsconfig.app.json
================================================
{
  "extends": "@vue/tsconfig/tsconfig.web.json",
  "include": ["src/env.d.tsd.ts", "src/**/*", "src/**/*.vue"],
  "exclude": ["src/**/__tests__/*"],
  "compilerOptions": {
    "composite": true,
    "outDir": "dist",
    "allowJs": true,
    "baseUrl": ".",
    "rootDir": "src",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}


================================================
FILE: tsconfig.config.json
================================================
{
  "extends": "@vue/tsconfig/tsconfig.node.json",
  "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
  "compilerOptions": {
    "composite": true,
    "types": ["node"]
  }
}


================================================
FILE: tsconfig.json
================================================
{
  "files": [],
  "references": [
    {
      "path": "./tsconfig.config.json"
    },
    {
      "path": "./tsconfig.app.json"
    },
    {
      "path": "./tsconfig.vitest.json"
    }
  ]
}


================================================
FILE: tsconfig.vitest.json
================================================
{
  "extends": "./tsconfig.app.json",
  "exclude": [],
  "compilerOptions": {
    "composite": true,
    "lib": [],
    "types": ["node", "jsdom"]
  }
}


================================================
FILE: vite.config.ts
================================================
/// <reference types="vitest" />
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'

// https://vitejs.dev/config/
export default defineConfig({
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
  build: {
    lib: {
      entry: fileURLToPath(new URL('./src/index.ts', import.meta.url)),
      name: 'VueComixable',
      formats: ['es', 'cjs', 'iife'],
      fileName: (format) => {
        switch (format) {
          case 'es':
            return 'index.mjs'
          case 'cjs':
            return 'index.cjs'
          case 'iife':
            return 'index.js'
          default:
            return 'index.js'
        }
      },
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: {
          vue: 'Vue',
        },
      },
    },
  },
  test: {
    setupFiles: ['./src/__tests__/setup.ts'],
  },
})
Download .txt
gitextract_tjv42v88/

├── .eslintignore
├── .eslintrc.cjs
├── .github/
│   └── workflows/
│       ├── ci.yml
│       └── size.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── .release-it.json
├── .vscode/
│   └── extensions.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── commitlint.config.js
├── index.html
├── package.json
├── src/
│   ├── __tests__/
│   │   ├── advanced.spec.ts
│   │   ├── cache.spec.ts
│   │   ├── computed.spec.ts
│   │   ├── data.spec.ts
│   │   ├── helpers.ts
│   │   ├── inject.spec.ts
│   │   ├── lifecylce.spec.ts
│   │   ├── methods.spec.ts
│   │   ├── propsEmits.spec.ts
│   │   ├── setup.ts
│   │   └── watch.spec.ts
│   ├── createComposable.ts
│   ├── defineMixin.ts
│   ├── env.d.ts
│   ├── index.ts
│   ├── inject.ts
│   ├── typeTest.ts
│   ├── types.ts
│   ├── utils.ts
│   └── vmContextProxy.ts
├── tsconfig.app.json
├── tsconfig.config.json
├── tsconfig.json
├── tsconfig.vitest.json
└── vite.config.ts
Download .txt
SYMBOL INDEX (45 symbols across 14 files)

FILE: src/__tests__/advanced.spec.ts
  method created (line 19) | created() {
  method mounted (line 22) | mounted() {
  method getNestedProperty (line 36) | getNestedProperty(): string {
  method mounted (line 40) | mounted() {

FILE: src/__tests__/computed.spec.ts
  method msg (line 11) | msg() {
  method get (line 31) | get(): string {
  method set (line 34) | set(v: string) {
  method msg (line 59) | msg() {
  method msg (line 78) | msg(): string {

FILE: src/__tests__/helpers.ts
  method setup (line 13) | setup() {

FILE: src/__tests__/inject.spec.ts
  method data (line 17) | data(): { msg: string } {

FILE: src/__tests__/lifecylce.spec.ts
  function testHandler (line 9) | function testHandler() {
  method beforeCreate (line 51) | beforeCreate() {
  method created (line 54) | created() {

FILE: src/__tests__/methods.spec.ts
  method msg (line 12) | msg() {
  method msg (line 28) | msg() {
  method getMsg (line 47) | getMsg() {

FILE: src/__tests__/watch.spec.ts
  method msg (line 16) | msg(...args: any[]) {

FILE: src/createComposable.ts
  function createComposableFromMixin (line 53) | function createComposableFromMixin<
  function createWatcher (line 232) | function createWatcher(
  function createPathGetter (line 262) | function createPathGetter(ctx: any, path: string) {

FILE: src/defineMixin.ts
  function defineMixin (line 10) | function defineMixin<

FILE: src/inject.ts
  type ComponentInjectOptions (line 16) | type ComponentInjectOptions = string[] | ObjectInjectOptions
  type ObjectInjectOptions (line 18) | type ObjectInjectOptions = Record<
  type DebuggerHook (line 22) | type DebuggerHook = (e: DebuggerEvent) => void
  type ErrorCapturedHook (line 23) | type ErrorCapturedHook<TError = unknown> = (
  function resolveInjections (line 31) | function resolveInjections(
  function normalizeInject (line 68) | function normalizeInject(

FILE: src/typeTest.ts
  type User (line 8) | interface User {
  method birthYear (line 30) | birthYear(): number {
  method testFn (line 35) | testFn(): void {
  method setup (line 45) | setup() {
  type TCompWMixin (line 63) | type TCompWMixin = InstanceType<typeof CompWMixin>
  type checkAge (line 64) | type checkAge = TCompWMixin['age']
  type checkUser (line 65) | type checkUser = TCompWMixin['user']

FILE: src/types.ts
  type EmitsToProps (line 18) | type EmitsToProps<T extends EmitsOptions> = T extends string[]
  type MethodOptions (line 37) | interface MethodOptions<Context = ComponentPublicInstance> {
  type ComputedOptions (line 44) | type ComputedOptions = Record<
  type ContextualizedComputedOptions (line 48) | type ContextualizedComputedOptions<Context, T = any> = Record<
  type ExtractComputedReturns (line 56) | type ExtractComputedReturns<T extends any> = {
  type ObjectWatchOptionItem (line 64) | type ObjectWatchOptionItem = {
  type WatchOptionItem (line 67) | type WatchOptionItem = string | WatchCallback | ObjectWatchOptionItem
  type ComponentWatchOptionItem (line 68) | type ComponentWatchOptionItem = WatchOptionItem | WatchOptionItem[]

FILE: src/utils.ts
  function callHook (line 19) | function callHook(

FILE: src/vmContextProxy.ts
  function createContextProxy (line 3) | function createContextProxy(
Condensed preview — 40 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (51K chars).
[
  {
    "path": ".eslintignore",
    "chars": 15,
    "preview": "/coverage\n/dist"
  },
  {
    "path": ".eslintrc.cjs",
    "chars": 284,
    "preview": "/* eslint-env node */\nmodule.exports = {\n  root: true,\n  extends: ['@linusborg/eslint-config', 'plugin:vue/vue3-essentia"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 1294,
    "preview": "name: 'ci'\non:\n  push:\n    branches:\n      - '**'\n  pull_request:\n    branches:\n      - main\n\npermissions:\n  contents: r"
  },
  {
    "path": ".github/workflows/size.yml",
    "chars": 349,
    "preview": "name: \"size\"\non:\n  pull_request:\n    branches:\n      - main\njobs:\n  size:\n    runs-on: ubuntu-latest\n    env:\n      CI_J"
  },
  {
    "path": ".gitignore",
    "chars": 327,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\n.DS_Stor"
  },
  {
    "path": ".prettierignore",
    "chars": 12,
    "preview": "dist/\n/types"
  },
  {
    "path": ".prettierrc.json",
    "chars": 88,
    "preview": "{\n  \"useTabs\": false,\n  \"singleQuote\": true,\n  \"trailingComma\": \"es5\",\n  \"semi\": false\n}"
  },
  {
    "path": ".release-it.json",
    "chars": 415,
    "preview": "{\n  \"git\": {\n    \"commitMessage\": \"chore: release v${version}\"\n  },\n  \"github\": {\n    \"release\": true\n  },\n  \"plugins\": "
  },
  {
    "path": ".vscode/extensions.json",
    "chars": 75,
    "preview": "{\n  \"recommendations\": [\"Vue.volar\", \"Vue.vscode-typescript-vue-plugin\"]\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 1872,
    "preview": "\n\n## [0.3.1](https://github.com/LinusBorg/vue-mixable/compare/0.3.0...0.3.1) (2022-11-17)\n\n\n### Bug Fixes\n\n* **vm:** ens"
  },
  {
    "path": "LICENSE",
    "chars": 1083,
    "preview": "MIT License\n\nCopyright (c) 2022 - present Thorsten Lünborg\n\nPermission is hereby granted, free of charge, to any person "
  },
  {
    "path": "README.md",
    "chars": 7381,
    "preview": "![current npm version](https://img.shields.io/npm/v/vue-mixable) ![npm bundle size (min+gzip)](https://badgen.net/bundle"
  },
  {
    "path": "commitlint.config.js",
    "chars": 209,
    "preview": "const config = require('@commitlint/config-angular')\nmodule.exports = {\n  extends: ['@commitlint/config-angular'],\n  rul"
  },
  {
    "path": "index.html",
    "chars": 331,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\">\n    <link rel=\"icon\" href=\"/favicon.ico\">\n    <meta"
  },
  {
    "path": "package.json",
    "chars": 2611,
    "preview": "{\n  \"name\": \"vue-mixable\",\n  \"description\": \"Turn Vue Mixins into Composables with a simple helper function\",\n  \"version"
  },
  {
    "path": "src/__tests__/advanced.spec.ts",
    "chars": 1776,
    "preview": "import { describe, test, expect, vi } from 'vitest'\nimport { createComposableFromMixin } from '../createComposable'\nimpo"
  },
  {
    "path": "src/__tests__/cache.spec.ts",
    "chars": 489,
    "preview": "import { describe, test, expect } from 'vitest'\nimport { createComposableFromMixin } from '../createComposable'\nimport {"
  },
  {
    "path": "src/__tests__/computed.spec.ts",
    "chars": 2150,
    "preview": "import { describe, test, expect } from 'vitest'\nimport { createComposableFromMixin } from '../createComposable'\nimport {"
  },
  {
    "path": "src/__tests__/data.spec.ts",
    "chars": 1666,
    "preview": "import { describe, test, expect } from 'vitest'\nimport { nextTick } from 'vue'\nimport { createComposableFromMixin } from"
  },
  {
    "path": "src/__tests__/helpers.ts",
    "chars": 469,
    "preview": "import { mount, type MountingOptions } from '@vue/test-utils'\nimport { defineComponent } from 'vue'\n\nexport const wrapCo"
  },
  {
    "path": "src/__tests__/inject.spec.ts",
    "chars": 1552,
    "preview": "import { describe, test, expect } from 'vitest'\nimport { defineComponent, ref } from 'vue'\nimport { createComposableFrom"
  },
  {
    "path": "src/__tests__/lifecylce.spec.ts",
    "chars": 1745,
    "preview": "import { describe, test, expect, vi } from 'vitest'\nimport { createComposableFromMixin } from '../createComposable'\nimpo"
  },
  {
    "path": "src/__tests__/methods.spec.ts",
    "chars": 1461,
    "preview": "import { describe, test, expect } from 'vitest'\nimport type { ComponentPublicInstance } from 'vue'\nimport { createCompos"
  },
  {
    "path": "src/__tests__/propsEmits.spec.ts",
    "chars": 1394,
    "preview": "import { describe, test, expect, vi } from 'vitest'\nimport { defineComponent } from 'vue'\nimport { mount } from '@vue/te"
  },
  {
    "path": "src/__tests__/setup.ts",
    "chars": 88,
    "preview": "import { config } from '@vue/test-utils'\n\nconfig.global.config.unwrapInjectedRef = true\n"
  },
  {
    "path": "src/__tests__/watch.spec.ts",
    "chars": 1024,
    "preview": "import { describe, test, expect, vi } from 'vitest'\nimport { nextTick } from 'vue'\nimport { createComposableFromMixin } "
  },
  {
    "path": "src/createComposable.ts",
    "chars": 6710,
    "preview": "/**\n * Code in this file is based on the Options API implementation in Vue 3's codebase:\n * https://github.com/vuejs/cor"
  },
  {
    "path": "src/defineMixin.ts",
    "chars": 724,
    "preview": "import type {\n  ComputedOptions,\n  MethodOptions,\n  ComponentOptionsWithObjectProps,\n  ComponentOptionsMixin,\n  Componen"
  },
  {
    "path": "src/env.d.ts",
    "chars": 38,
    "preview": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "src/index.ts",
    "chars": 107,
    "preview": "export { createComposableFromMixin } from './createComposable'\nexport { defineMixin } from './defineMixin'\n"
  },
  {
    "path": "src/inject.ts",
    "chars": 2095,
    "preview": "/**\n * @legal\n * This code is largely based on code from the Vue 3 codebase:\n * https://github.com/vuejs/core/blob/f67bb"
  },
  {
    "path": "src/typeTest.ts",
    "chars": 1386,
    "preview": "/**\n * This file is not to be used anywhere, it's just to test the type inference\n */\nimport { defineComponent } from 'v"
  },
  {
    "path": "src/types.ts",
    "chars": 1928,
    "preview": "/**\n * @legal\n * Various types in this file are based on code from the Vue 3 codebase:\n * https://github.com/vuejs/core/"
  },
  {
    "path": "src/utils.ts",
    "chars": 910,
    "preview": "import {\n  callWithAsyncErrorHandling,\n  type ComponentInternalInstance,\n  type ComponentPublicInstance,\n} from 'vue'\n\ne"
  },
  {
    "path": "src/vmContextProxy.ts",
    "chars": 1233,
    "preview": "import type { ComponentPublicInstance } from 'vue'\n\nexport /* #__PURE__ */ function createContextProxy(\n  _vm: Component"
  },
  {
    "path": "tsconfig.app.json",
    "chars": 333,
    "preview": "{\n  \"extends\": \"@vue/tsconfig/tsconfig.web.json\",\n  \"include\": [\"src/env.d.tsd.ts\", \"src/**/*\", \"src/**/*.vue\"],\n  \"excl"
  },
  {
    "path": "tsconfig.config.json",
    "chars": 219,
    "preview": "{\n  \"extends\": \"@vue/tsconfig/tsconfig.node.json\",\n  \"include\": [\"vite.config.*\", \"vitest.config.*\", \"cypress.config.*\","
  },
  {
    "path": "tsconfig.json",
    "chars": 193,
    "preview": "{\n  \"files\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.config.json\"\n    },\n    {\n      \"path\": \"./tsconfig.a"
  },
  {
    "path": "tsconfig.vitest.json",
    "chars": 153,
    "preview": "{\n  \"extends\": \"./tsconfig.app.json\",\n  \"exclude\": [],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"lib\": [],\n    "
  },
  {
    "path": "vite.config.ts",
    "chars": 923,
    "preview": "/// <reference types=\"vitest\" />\nimport { fileURLToPath, URL } from 'node:url'\nimport { defineConfig } from 'vite'\n\n// h"
  }
]

About this extraction

This page contains the full source code of the LinusBorg/vue-mixable GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 40 files (46.0 KB), approximately 13.3k tokens, and a symbol index with 45 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!