├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docker-compose.yaml ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── RedisCacheAdapter.ts └── index.ts ├── test └── RedisCacheAdapter.test.ts ├── tsconfig.json └── vite.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | rollup.config.js 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2021: true, 4 | node: true, 5 | }, 6 | extends: [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:prettier/recommended", 10 | ], 11 | parser: "@typescript-eslint/parser", 12 | parserOptions: { 13 | ecmaVersion: "latest", 14 | sourceType: "module", 15 | }, 16 | plugins: ["@typescript-eslint", "prettier"], 17 | rules: {}, 18 | }; 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Build and Test 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | branches: [main] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [18.x, 20.x] 19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: 'npm' 28 | 29 | - run: npm ci 30 | 31 | - name: Start test services 32 | run: npm run test:services:start 33 | 34 | - run: npm run lint 35 | 36 | - name: Run tests 37 | run: npm test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/node 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | .env.test 79 | .env*.local 80 | 81 | # parcel-bundler cache (https://parceljs.org/) 82 | .cache 83 | .parcel-cache 84 | 85 | # Next.js build output 86 | .next 87 | 88 | # Nuxt.js build / generate output 89 | .nuxt 90 | dist 91 | 92 | # Gatsby files 93 | .cache/ 94 | # Comment in the public line in if your project uses Gatsby and not Next.js 95 | # https://nextjs.org/blog/next-9-1#public-directory-support 96 | # public 97 | 98 | # vuepress build output 99 | .vuepress/dist 100 | 101 | # Serverless directories 102 | .serverless/ 103 | 104 | # FuseBox cache 105 | .fusebox/ 106 | 107 | # DynamoDB Local files 108 | .dynamodb/ 109 | 110 | # TernJS port file 111 | .tern-port 112 | 113 | # Stores VSCode versions used for testing VSCode extensions 114 | .vscode-test 115 | 116 | # End of https://www.toptal.com/developers/gitignore/api/node 117 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Mikro ORM - Redis cache adapter - Changelog 2 | 3 | ## 3.2.0 4 | 5 | - Logger function is now customizable (by @kaname-png) 6 | 7 | ## 3.1.0 8 | 9 | - Fix interface for mikroORM v5 10 | 11 | ## 3.0.0 12 | 13 | - Support mikroORM v5 14 | 15 | ## 2.0.0 16 | 17 | - Support for lazy connected redis clients 18 | 19 | ## 1.1.1 20 | 21 | - Remove log 22 | 23 | ## 1.1.0 24 | 25 | - Ignore cache when redis is not connected 26 | 27 | ## 1.0.1 28 | 29 | - Expose adapter as default 30 | - Update mikro-orm dependency 31 | 32 | ## 1.0.0 33 | 34 | - Accept a redis client instance directly 35 | - IORedis is now a peer dependency 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Fabrizio Ruggeri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mikro-orm - Redis cache adapter 2 | 3 | This is an adapter for redis to be used with mikro-orm. 4 | 5 | Install it with its peer dependencies 6 | 7 | ``` 8 | npm i mikro-orm-cache-adapter-redis ioredis 9 | ``` 10 | 11 | and pass it as option to mikro-orm 12 | 13 | ```js 14 | import { MikroORM } from '@mikro-orm/core/MikroORM'; 15 | import { RedisCacheAdapter } from 'mikro-orm-cache-adapter-redis'; 16 | 17 | const orm = await MikroORM.init({ 18 | // Your options 19 | resultCache: { 20 | adapter: RedisCacheAdapter, 21 | options: { 22 | // Base options 23 | // An optional key prefix. By default is `mikro` 24 | keyPrefix: 'mikro' 25 | // Optional: print debug informations 26 | debug: false, 27 | 28 | 29 | // Here goes IORedis connection options (the library will instantiate the client) 30 | host: '...', 31 | port: 6379, 32 | password: 'yourpassword' 33 | } 34 | } 35 | }); 36 | ``` 37 | 38 | Instead of passing options, you can pass directly an IORedis instance 39 | 40 | ```js 41 | import { RedisCacheAdapter } from "mikro-orm-cache-adapter-redis"; 42 | import Redis from "ioredis"; 43 | 44 | const myRedisClient = new Redis(); 45 | 46 | const orm = await MikroORM.init({ 47 | // Your options 48 | resultCache: { 49 | adapter: RedisCacheAdapter, 50 | options: { 51 | client: myRedisClient, 52 | }, 53 | }, 54 | }); 55 | ``` 56 | 57 | ## Serializing 58 | 59 | This package uses [`serialize`](https://nodejs.org/api/v8.html#v8serializevalue) and [`deserialize`](https://nodejs.org/api/v8.html#v8deserializebuffer) functions from the Node.js v8 API instead of `JSON.stringify` and `JSON.parse`. 60 | 61 | They are inadequate for certain primitive data types like Buffer and Typed Array, as they cannot accurately reproduce same data after serialization. 62 | You can checkout its limitation [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#description). 63 | 64 | But, there're still some primitives that `serialize` cannot handle. 65 | 66 | - function 67 | - symbol 68 | - any uncopyable data 69 | 70 | If you need to serialize these types of data, you should using a custom [serializer](https://mikro-orm.io/docs/serializing#property-serializers) or [custom type](https://mikro-orm.io/docs/custom-types) 71 | 72 | If you're in debug mode, you will see JSON stringified data at your console. This is solely for debugging purposes. `serialize` is used for actual cache. 73 | 74 | ## Testing 75 | 76 | You need docker compose to run the tests. 77 | 78 | ```bash 79 | # Start test services 80 | npm run test:services:start 81 | 82 | # Run tests 83 | npm test 84 | ``` -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | redis: 3 | image: redis:alpine 4 | ports: 5 | - "6379:6379" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mikro-orm-cache-adapter-redis", 3 | "version": "4.0.0", 4 | "description": "A redis cache adapter for mikro-orm", 5 | "main": "dist/index.cjs.js", 6 | "module": "dist/index.esm.js", 7 | "types": "dist/index.d.ts", 8 | "repository": "https://github.com/ramiel/mikro-orm-cache-adapter-redis", 9 | "author": "Fabrizio Ruggeri", 10 | "license": "MIT", 11 | "scripts": { 12 | "prepare": "npm run build", 13 | "build": "rollup -c --environment NODE_ENV:production", 14 | "build-watch": "rollup -c --environment NODE_ENV:production -w --no-watch.clearScreen", 15 | "lint": "eslint .", 16 | "test": "vitest run --coverage", 17 | "test:services:start": "docker compose -f docker-compose.yaml up -d", 18 | "test:services:stop": "docker compose -f docker-compose.yaml down" 19 | }, 20 | "devDependencies": { 21 | "@mikro-orm/core": "^5.0.5", 22 | "@typescript-eslint/eslint-plugin": "^5.15.0", 23 | "@typescript-eslint/parser": "^5.15.0", 24 | "@vitest/coverage-v8": "^2.0.5", 25 | "eslint": "^8.11.0", 26 | "eslint-config-prettier": "^8.5.0", 27 | "eslint-plugin-prettier": "^4.0.0", 28 | "ioredis": "^5.3.2", 29 | "prettier": "2.6.0", 30 | "rollup": "^2.42.4", 31 | "rollup-plugin-typescript2": "0.31.0", 32 | "typescript": "^4.1.3", 33 | "vitest": "^2.0.5" 34 | }, 35 | "peerDependencies": { 36 | "ioredis": "^5.3.2" 37 | }, 38 | "files": [ 39 | "dist" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | // import { nodeResolve } from '@rollup/plugin-node-resolve'; 3 | // import commonjs from 'rollup-plugin-commonjs'; 4 | // import resolve from 'rollup-plugin-node-resolve'; 5 | import pkg from './package.json'; 6 | import typescript from 'typescript'; 7 | import typescriptPlugin from 'rollup-plugin-typescript2'; 8 | 9 | const isProd = process.env.NODE_ENV === 'production'; 10 | 11 | const externalDepsRegexp = Object.keys(pkg.peerDependencies || {}).map( 12 | (dep) => new RegExp(`${dep}(/.+)?`), 13 | ); 14 | 15 | export default [ 16 | { 17 | input: 'src/index.ts', 18 | external: (id) => { 19 | // We consider external any dep in peerDependecies or a sub import 20 | // e.g. reakit, reakit/Portal 21 | return externalDepsRegexp.reduce((acc, regexp) => { 22 | return acc || regexp.test(id); 23 | }, false); 24 | }, 25 | plugins: [ 26 | typescriptPlugin({ 27 | clean: isProd, 28 | typescript, 29 | }), 30 | ], 31 | output: [ 32 | { file: pkg.module, format: 'es' }, 33 | { file: pkg.main, format: 'cjs' }, 34 | ], 35 | }, 36 | ]; 37 | -------------------------------------------------------------------------------- /src/RedisCacheAdapter.ts: -------------------------------------------------------------------------------- 1 | import { serialize, deserialize } from "node:v8"; 2 | import IORedis from "ioredis"; 3 | 4 | import type { CacheAdapter } from "@mikro-orm/core"; 5 | import type { Redis, RedisOptions } from "ioredis"; 6 | 7 | export interface BaseOptions { 8 | expiration?: number; 9 | keyPrefix?: string; 10 | debug?: boolean; 11 | } 12 | 13 | export interface BuildOptions extends BaseOptions, RedisOptions {} 14 | export interface ClientOptions extends BaseOptions { 15 | client: Redis; 16 | logger: (...args: unknown[]) => void; 17 | } 18 | 19 | export type RedisCacheAdapterOptions = BuildOptions | ClientOptions; 20 | 21 | export class RedisCacheAdapter implements CacheAdapter { 22 | private readonly client: Redis; 23 | private readonly debug: boolean; 24 | private readonly expiration?: number; 25 | private readonly keyPrefix: string; 26 | private readonly logger: (...args: unknown[]) => void; 27 | 28 | constructor(options: RedisCacheAdapterOptions) { 29 | const { debug = false, expiration, keyPrefix = "" } = options; 30 | this.logger = (options as ClientOptions).logger ?? console.log; 31 | 32 | this.keyPrefix = this.computeKeyPrefix(options, keyPrefix); 33 | this.client = this.createRedisClient(options); 34 | this.debug = debug; 35 | this.expiration = expiration; 36 | this.logDebugMessage( 37 | `The Redis client for cache has been created! | Cache expiration: ${this.expiration}ms` 38 | ); 39 | } 40 | 41 | private computeKeyPrefix( 42 | options: RedisCacheAdapterOptions, 43 | localKeyPrefix: string 44 | ): string { 45 | if ((options as ClientOptions).client?.options.keyPrefix) { 46 | return localKeyPrefix ? `:${localKeyPrefix}` : ""; 47 | } 48 | 49 | // Default to 'mikro' if keyPrefix is not provided and there's no client options key prefix 50 | return localKeyPrefix ?? "mikro"; 51 | } 52 | 53 | private createRedisClient(options: RedisCacheAdapterOptions): Redis { 54 | if ((options as ClientOptions).client) { 55 | return (options as ClientOptions).client; 56 | } else { 57 | const redisOpt = options as BuildOptions; 58 | return new IORedis(redisOpt); 59 | } 60 | } 61 | 62 | private logDebugMessage(message: string) { 63 | if (this.debug) { 64 | this.logger(message); 65 | } 66 | } 67 | 68 | _getKey(name: string) { 69 | return `${this.keyPrefix}:${name}`; 70 | } 71 | 72 | async get(key: string): Promise { 73 | const completeKey = this._getKey(key); 74 | 75 | let data: Buffer | null; 76 | try { 77 | data = await this.client.getBuffer(completeKey); 78 | } catch (e) { 79 | this.logDebugMessage(`Failed to get the data: ${completeKey}`); 80 | return undefined; 81 | } 82 | 83 | if (!data) { 84 | this.logDebugMessage(`Get "${completeKey}": "undefined"`); 85 | return undefined; 86 | } 87 | 88 | let deserialized: T; 89 | try { 90 | deserialized = deserialize(data) as T; 91 | } catch (error) { 92 | this.logDebugMessage(`Failed to deserialize the data of ${key}`); 93 | return undefined; 94 | } 95 | 96 | this.logDebugMessage( 97 | `Get "${completeKey}": "${JSON.stringify(deserialized)}"` 98 | ); 99 | 100 | return deserialized; 101 | } 102 | 103 | async set( 104 | key: string, 105 | data: unknown, 106 | _origin: string, 107 | expiration = this.expiration 108 | ): Promise { 109 | let serialized: Buffer; 110 | try { 111 | serialized = serialize(data); 112 | } catch (error) { 113 | this.logDebugMessage(`Failed to serialize data: ${data}`); 114 | return; 115 | } 116 | 117 | const completeKey = this._getKey(key); 118 | 119 | try { 120 | if (expiration) { 121 | await this.client.set(completeKey, serialized, "PX", expiration); 122 | } else { 123 | await this.client.set(completeKey, serialized); 124 | } 125 | } catch (e) { 126 | this.logDebugMessage( 127 | `Error while setting key cache =>> ${(e as Error).message}` 128 | ); 129 | return; 130 | } 131 | 132 | this.logDebugMessage( 133 | `Set "${completeKey}": "${JSON.stringify( 134 | data 135 | )}" with cache expiration ${expiration}ms` 136 | ); 137 | } 138 | 139 | async remove(name: string): Promise { 140 | const completeKey = this._getKey(name); 141 | this.logDebugMessage(`Remove specific key cache =>> ${completeKey}`); 142 | 143 | try { 144 | await this.client.del(completeKey); 145 | } catch (e) { 146 | this.logDebugMessage( 147 | `Error while removing key cache =>> ${(e as Error).message}` 148 | ); 149 | throw new CacheRemoveError(e as Error); 150 | } 151 | } 152 | 153 | async clear(): Promise { 154 | this.logDebugMessage("Clearing cache..."); 155 | return new Promise((resolve, reject) => { 156 | const stream = this.client.scanStream({ 157 | match: `${this.keyPrefix}:*`, 158 | }); 159 | const pipeline = this.client.pipeline(); 160 | stream.on("data", (keys: string[]) => { 161 | if (keys.length) { 162 | keys.forEach(function (key) { 163 | pipeline.del(key); 164 | }); 165 | } 166 | }); 167 | stream.on("end", () => { 168 | pipeline.exec((err) => { 169 | if (err) { 170 | this.logDebugMessage("Error clearing cache"); 171 | return reject(err); 172 | } 173 | this.logDebugMessage("Cleared cache"); 174 | resolve(); 175 | }); 176 | }); 177 | }); 178 | } 179 | 180 | async close() { 181 | this.client.disconnect(); 182 | } 183 | } 184 | 185 | export class CacheRemoveError extends Error { 186 | constructor(cause: Error) { 187 | super("Cache remove failed", { cause }); 188 | } 189 | } 190 | 191 | export default RedisCacheAdapter; 192 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./RedisCacheAdapter"; 2 | -------------------------------------------------------------------------------- /test/RedisCacheAdapter.test.ts: -------------------------------------------------------------------------------- 1 | import { it, vi, expect, beforeEach } from "vitest"; 2 | import { CacheRemoveError, RedisCacheAdapter } from "../src"; 3 | import IORedis from "ioredis"; 4 | 5 | const client = new IORedis("redis://localhost:6379"); 6 | const adapter = new RedisCacheAdapter({ 7 | client, 8 | logger: vi.fn(), 9 | }); 10 | 11 | beforeEach(async () => { 12 | await client.flushall(); 13 | }); 14 | 15 | it.each([ 16 | { test: "test" }, 17 | { 18 | a: 123123, 19 | c: true, 20 | d: false, 21 | e: { a: 1, b: 2 }, 22 | f: /^test$/, 23 | }, 24 | { 25 | a: [1, 2, 3], 26 | b: Buffer.from([1, 2, 3]), 27 | ta: new Uint8Array([1, 2, 3]), 28 | d: new Date(), 29 | e: new Error(), 30 | f: new Map([ 31 | [1, 2], 32 | [3, 4], 33 | ]), 34 | g: new Set([1, 2, 3]), 35 | }, 36 | ])(`Save a cache and load it: %o`, async (data) => { 37 | await adapter.set("key", data, "origin"); 38 | 39 | const result = await adapter.get("key"); 40 | 41 | expect(result).toEqual(data); 42 | }); 43 | 44 | it("Failed to serialize the data, return undefined", async () => { 45 | await adapter.set("key", { a: () => undefined }, "origin"); 46 | 47 | const result = await adapter.get("key"); 48 | 49 | expect(result).toBe(undefined); 50 | }); 51 | 52 | it("Failed to save a cache, return undefined", async () => { 53 | vi.spyOn(client, "set").mockRejectedValueOnce(new Error()); 54 | 55 | await adapter.set("key", "test", "origin"); 56 | 57 | const result = await adapter.get("key"); 58 | 59 | expect(result).toBe(undefined); 60 | }); 61 | 62 | it("Failed to load the cache, return undefined", async () => { 63 | vi.spyOn(client, "getBuffer").mockRejectedValueOnce(new Error()); 64 | 65 | const result = await adapter.get("key"); 66 | 67 | expect(result).toBe(undefined); 68 | }); 69 | 70 | it("When cache is not found, return undefined", async () => { 71 | const result = await adapter.get("key"); 72 | 73 | expect(result).toBe(undefined); 74 | }); 75 | 76 | it("Failed to deserialize the data, return undefined", async () => { 77 | await adapter.set("key", "test", "origin"); 78 | vi.spyOn(client, "getBuffer").mockResolvedValueOnce(Buffer.from("test")); 79 | 80 | const result = await adapter.get("key"); 81 | 82 | expect(result).toBe(undefined); 83 | }); 84 | 85 | it("Failed to remove the cache, throw CacheRemoveError", async () => { 86 | vi.spyOn(client, "del").mockRejectedValueOnce(new Error()); 87 | 88 | expect(adapter.remove("key")).rejects.toThrowError(CacheRemoveError); 89 | }); 90 | 91 | it("Clear all cache", async () => { 92 | await adapter.set("key1", "test", "origin"); 93 | await adapter.set("key2", "test", "origin"); 94 | 95 | await adapter.clear(); 96 | 97 | const result = await adapter.get("key1"); 98 | const result2 = await adapter.get("key2"); 99 | 100 | expect(result).toBe(undefined); 101 | expect(result2).toBe(undefined); 102 | }); 103 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* type checking */ 4 | "strict": true, 5 | "noUnusedLocals": true, 6 | "useUnknownInCatchVariables": true, 7 | 8 | /* modules */ 9 | "module": "ESNext", 10 | "moduleResolution": "Node", 11 | 12 | /* Language and Environment */ 13 | "lib": ["ESNext"], 14 | "target": "es5", 15 | 16 | /* Emit */ 17 | "outDir": "./dist", 18 | "declaration": true, 19 | 20 | /* Interop Constraints */ 21 | "esModuleInterop": true, 22 | "forceConsistentCasingInFileNames": true, 23 | 24 | "skipLibCheck": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ["test/*.ts"], 6 | coverage: { 7 | reporter: ["text", "json", "html"], 8 | }, 9 | }, 10 | }); 11 | --------------------------------------------------------------------------------