├── .github ├── release-drafter.yml └── workflows │ ├── build.yml │ ├── release.yml │ └── stale.yml ├── .gitignore ├── .npmignore ├── biome.json ├── index.ts ├── index_test.ts ├── license ├── package.json ├── readme.md ├── testdata └── server.ts ├── tsconfig.json └── vite.config.ts /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: "v$RESOLVED_VERSION" 2 | tag-template: "v$RESOLVED_VERSION" 3 | template: | 4 | $CHANGES 5 | category-template: "#### $TITLE" 6 | change-template: "* #$NUMBER - $TITLE (@$AUTHOR)" 7 | categories: 8 | - title: "Breaking changes" 9 | label: "breaking" 10 | - title: "Enhancements" 11 | label: "enhancement" 12 | - title: "Bug fixes" 13 | label: "bug" 14 | - title: "Maintenance" 15 | label: "chore" 16 | 17 | version-resolver: 18 | major: 19 | labels: 20 | - "breaking" 21 | minor: 22 | labels: 23 | - "enhancement" 24 | patch: 25 | labels: 26 | - "bug" 27 | - "chore" 28 | 29 | exclude-labels: 30 | - "skip-changelog" 31 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node: [18, 20, 22, 24] 13 | name: Node v${{ matrix.node }} 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: ${{ matrix.node }} 19 | - run: sudo apt-get install -y redis-server 20 | - run: npm install 21 | - run: npm run lint 22 | - run: npm test 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release-draft 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | update_release_draft: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: release-drafter/release-drafter@master 12 | env: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: stale-issues-prs 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v3 11 | with: 12 | stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days." 13 | stale-pr-message: "This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days." 14 | close-issue-message: "This issue was closed because it has been stalled for 5 days with no activity." 15 | days-before-stale: 30 16 | days-before-close: 5 17 | stale-issue-label: stale 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | yarn-error.log 5 | package-lock.json 6 | yarn.lock 7 | .DS_Store 8 | dump.rdb 9 | pnpm-lock.yaml 10 | dist 11 | pnpm-workspace.yaml 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | testdata 2 | coverage 3 | .nyc_output 4 | 5 | package-lock.json 6 | yarn.lock 7 | pnpm-lock.yaml 8 | 9 | dump.rdb 10 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "ignore": ["dist", "pnpm-*.yaml", "coverage"] 4 | }, 5 | "formatter": { 6 | "enabled": true, 7 | "indentStyle": "space", 8 | "bracketSpacing": false 9 | }, 10 | "linter": { 11 | "enabled": true, 12 | "rules": { 13 | "a11y": { 14 | "all": false 15 | }, 16 | "style": { 17 | "useConst": "off", 18 | "useTemplate": "off", 19 | "noParameterAssign": "off", 20 | "useSingleVarDeclarator": "off" 21 | }, 22 | "correctness": { 23 | "noUnusedImports": "error" 24 | }, 25 | "suspicious": { 26 | "noExplicitAny": "off", 27 | "noArrayIndexKey": "off", 28 | "noImplicitAnyLet": "off" 29 | } 30 | } 31 | }, 32 | "javascript": { 33 | "formatter": { 34 | "semicolons": "asNeeded" 35 | } 36 | }, 37 | "organizeImports": { 38 | "enabled": true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import {type SessionData, Store} from "express-session" 2 | import type {RedisClientType, RedisClusterType} from "redis" 3 | 4 | type Callback = (_err?: unknown, _data?: any) => any 5 | 6 | function optionalCb(err: unknown, data: unknown, cb?: Callback) { 7 | if (cb) return cb(err, data) 8 | if (err) throw err 9 | return data 10 | } 11 | 12 | interface Serializer { 13 | parse(s: string): SessionData | Promise<SessionData> 14 | stringify(s: SessionData): string 15 | } 16 | 17 | interface RedisStoreOptions { 18 | client: any 19 | prefix?: string 20 | scanCount?: number 21 | serializer?: Serializer 22 | ttl?: number | ((sess: SessionData) => number) 23 | disableTTL?: boolean 24 | disableTouch?: boolean 25 | } 26 | 27 | export class RedisStore extends Store { 28 | client: RedisClientType | RedisClusterType 29 | prefix: string 30 | scanCount: number 31 | serializer: Serializer 32 | ttl: number | ((sess: SessionData) => number) 33 | disableTTL: boolean 34 | disableTouch: boolean 35 | 36 | constructor(opts: RedisStoreOptions) { 37 | super() 38 | this.prefix = opts.prefix == null ? "sess:" : opts.prefix 39 | this.scanCount = opts.scanCount || 100 40 | this.serializer = opts.serializer || JSON 41 | this.ttl = opts.ttl || 86400 // One day in seconds. 42 | this.disableTTL = opts.disableTTL || false 43 | this.disableTouch = opts.disableTouch || false 44 | this.client = opts.client 45 | } 46 | 47 | async get(sid: string, cb?: Callback) { 48 | let key = this.prefix + sid 49 | try { 50 | let data = await this.client.get(key) 51 | if (!data) return optionalCb(null, null, cb) 52 | return optionalCb(null, await this.serializer.parse(data), cb) 53 | } catch (err) { 54 | return optionalCb(err, null, cb) 55 | } 56 | } 57 | 58 | async set(sid: string, sess: SessionData, cb?: Callback) { 59 | let key = this.prefix + sid 60 | let ttl = this.getTTL(sess) 61 | try { 62 | if (ttl > 0) { 63 | let val = this.serializer.stringify(sess) 64 | if (this.disableTTL) await this.client.set(key, val) 65 | else 66 | await this.client.set(key, val, { 67 | expiration: {type: "EX", value: ttl}, 68 | }) 69 | return optionalCb(null, null, cb) 70 | } 71 | return this.destroy(sid, cb) 72 | } catch (err) { 73 | return optionalCb(err, null, cb) 74 | } 75 | } 76 | 77 | async touch(sid: string, sess: SessionData, cb?: Callback) { 78 | let key = this.prefix + sid 79 | if (this.disableTouch || this.disableTTL) return optionalCb(null, null, cb) 80 | try { 81 | await this.client.expire(key, this.getTTL(sess)) 82 | return optionalCb(null, null, cb) 83 | } catch (err) { 84 | return optionalCb(err, null, cb) 85 | } 86 | } 87 | 88 | async destroy(sid: string, cb?: Callback) { 89 | let key = this.prefix + sid 90 | try { 91 | await this.client.del([key]) 92 | return optionalCb(null, null, cb) 93 | } catch (err) { 94 | return optionalCb(err, null, cb) 95 | } 96 | } 97 | 98 | async clear(cb?: Callback) { 99 | try { 100 | let keys = await this.getAllKeys() 101 | if (!keys.length) return optionalCb(null, null, cb) 102 | await this.client.del(keys) 103 | return optionalCb(null, null, cb) 104 | } catch (err) { 105 | return optionalCb(err, null, cb) 106 | } 107 | } 108 | 109 | async length(cb?: Callback) { 110 | try { 111 | let keys = await this.getAllKeys() 112 | return optionalCb(null, keys.length, cb) 113 | } catch (err) { 114 | return optionalCb(err, null, cb) 115 | } 116 | } 117 | 118 | async ids(cb?: Callback) { 119 | let len = this.prefix.length 120 | try { 121 | let keys = await this.getAllKeys() 122 | return optionalCb( 123 | null, 124 | keys.map((k) => k.substring(len)), 125 | cb, 126 | ) 127 | } catch (err) { 128 | return optionalCb(err, null, cb) 129 | } 130 | } 131 | 132 | async all(cb?: Callback) { 133 | let len = this.prefix.length 134 | try { 135 | let keys = await this.getAllKeys() 136 | if (keys.length === 0) return optionalCb(null, [], cb) 137 | 138 | let data = await this.client.mGet(keys) 139 | let results = data.reduce((acc, raw, idx) => { 140 | if (!raw) return acc 141 | let sess = this.serializer.parse(raw) as any 142 | sess.id = keys[idx].substring(len) 143 | acc.push(sess) 144 | return acc 145 | }, [] as SessionData[]) 146 | return optionalCb(null, results, cb) 147 | } catch (err) { 148 | return optionalCb(err, null, cb) 149 | } 150 | } 151 | 152 | private getTTL(sess: SessionData) { 153 | if (typeof this.ttl === "function") { 154 | return this.ttl(sess) 155 | } 156 | 157 | let ttl 158 | if (sess?.cookie?.expires) { 159 | let ms = Number(new Date(sess.cookie.expires)) - Date.now() 160 | ttl = Math.ceil(ms / 1000) 161 | } else { 162 | ttl = this.ttl 163 | } 164 | return ttl 165 | } 166 | 167 | private async getAllKeys() { 168 | let pattern = this.prefix + "*" 169 | let set = new Set<string>() 170 | for await (let keys of this.scanIterator(pattern, this.scanCount)) { 171 | for (let key of keys) { 172 | set.add(key) 173 | } 174 | } 175 | return set.size > 0 ? Array.from(set) : [] 176 | } 177 | 178 | private scanIterator(match: string, count: number) { 179 | let client = this.client 180 | 181 | if (!("masters" in client)) { 182 | return client.scanIterator({MATCH: match, COUNT: count}) 183 | } 184 | 185 | return (async function* () { 186 | for (let master of client.masters) { 187 | let c = await client.nodeClient(master) 188 | for await (let keys of c.scanIterator({ 189 | COUNT: count, 190 | MATCH: match, 191 | })) { 192 | yield keys 193 | } 194 | } 195 | })() 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /index_test.ts: -------------------------------------------------------------------------------- 1 | import {Cookie} from "express-session" 2 | import {createClient} from "redis" 3 | import {expect, test} from "vitest" 4 | import {RedisStore} from "./" 5 | import * as redisSrv from "./testdata/server" 6 | 7 | test("setup", async () => { 8 | await redisSrv.connect() 9 | }) 10 | 11 | test("defaults", async () => { 12 | let client = createClient({url: `redis://localhost:${redisSrv.port}`}) 13 | await client.connect() 14 | 15 | let store = new RedisStore({client}) 16 | 17 | expect(store.client).toBeDefined() 18 | expect(store.prefix).toBe("sess:") 19 | expect(store.ttl).toBe(86400) // defaults to one day 20 | expect(store.scanCount).toBe(100) 21 | expect(store.serializer).toBe(JSON) 22 | expect(store.disableTouch).toBe(false) 23 | expect(store.disableTTL).toBe(false) 24 | client.destroy() 25 | }) 26 | 27 | test("redis", async () => { 28 | let client = createClient({url: `redis://localhost:${redisSrv.port}`}) 29 | await client.connect() 30 | let store = new RedisStore({client}) 31 | await lifecycleTest(store, client) 32 | client.destroy() 33 | }) 34 | 35 | test("teardown", redisSrv.disconnect) 36 | 37 | async function lifecycleTest(store: RedisStore, client: any): Promise<void> { 38 | let res = await store.clear() 39 | 40 | let sess = {foo: "bar", cookie: {originalMaxAge: null}} 41 | await store.set("123", sess) 42 | 43 | res = await store.get("123") 44 | expect(res).toEqual(sess) 45 | 46 | let ttl = await client.ttl("sess:123") 47 | expect(ttl).toBeGreaterThanOrEqual(86399) 48 | 49 | ttl = 60 50 | let expires = new Date(Date.now() + ttl * 1000) 51 | await store.set("456", {cookie: {originalMaxAge: null, expires}}) 52 | ttl = await client.ttl("sess:456") 53 | expect(ttl).toBeLessThanOrEqual(60) 54 | 55 | ttl = 90 56 | let expires2 = new Date(Date.now() + ttl * 1000) 57 | await store.touch("456", {cookie: {originalMaxAge: null, expires: expires2}}) 58 | ttl = await client.ttl("sess:456") 59 | expect(ttl).toBeGreaterThan(60) 60 | 61 | res = await store.length() 62 | expect(res).toBe(2) // stored two keys length 63 | 64 | res = await store.ids() 65 | res.sort() 66 | expect(res).toEqual(["123", "456"]) 67 | 68 | res = await store.all() 69 | res.sort((a: any, b: any) => (a.id > b.id ? 1 : -1)) 70 | expect(res).toEqual([ 71 | {id: "123", foo: "bar", cookie: {originalMaxAge: null}}, 72 | {id: "456", cookie: {originalMaxAge: null, expires: expires.toISOString()}}, 73 | ]) 74 | 75 | await store.destroy("456") 76 | res = await store.length() 77 | expect(res).toBe(1) // one key remains 78 | 79 | res = await store.clear() 80 | 81 | res = await store.length() 82 | expect(res).toBe(0) // no keys remain 83 | 84 | let count = 1000 85 | await load(store, count) 86 | 87 | res = await store.length() 88 | expect(res).toBe(count) 89 | 90 | await store.clear() 91 | res = await store.length() 92 | expect(res).toBe(0) 93 | 94 | expires = new Date(Date.now() + ttl * 1000) // expires in the future 95 | res = await store.set("789", {cookie: {originalMaxAge: null, expires}}) 96 | 97 | res = await store.length() 98 | expect(res).toBe(1) 99 | 100 | expires = new Date(Date.now() - ttl * 1000) // expires in the past 101 | await store.set("789", {cookie: {originalMaxAge: null, expires}}) 102 | 103 | res = await store.length() 104 | expect(res).toBe(0) // no key remains and that includes session 789 105 | } 106 | 107 | async function load(store: RedisStore, count: number) { 108 | let cookie = new Cookie() 109 | for (let sid = 0; sid < count; sid++) { 110 | cookie.expires = new Date(Date.now() + 1000) 111 | await store.set("s" + sid, {cookie}) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2010-2025 TJ Holowaychuk 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connect-redis", 3 | "description": "Redis session store for Connect", 4 | "version": "9.0.0", 5 | "author": "TJ Holowaychuk <tj@vision-media.ca>", 6 | "contributors": ["Marc Harter <wavded@gmail.com>"], 7 | "license": "MIT", 8 | "type": "module", 9 | "main": "./dist/connect-redis.cjs", 10 | "module": "./dist/connect-redis.js", 11 | "types": "./dist/connect-redis.d.ts", 12 | "exports": { 13 | ".": { 14 | "import": { 15 | "types": "./dist/connect-redis.d.ts", 16 | "default": "./dist/connect-redis.js" 17 | }, 18 | "require": { 19 | "types": "./dist/connect-redis.d.cts", 20 | "default": "./dist/connect-redis.cjs" 21 | } 22 | } 23 | }, 24 | "scripts": { 25 | "prepublishOnly": "vite build", 26 | "build": "vite build", 27 | "test": "vitest run --coverage", 28 | "lint": "tsc --noemit && biome check .", 29 | "fix": "biome check --write ." 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+ssh://git@github.com/tj/connect-redis.git" 34 | }, 35 | "devDependencies": { 36 | "@biomejs/biome": "^1.9.4", 37 | "@types/express-session": "^1.18.2", 38 | "@types/node": "^22.15.31", 39 | "@vitest/coverage-v8": "^3.2.3", 40 | "express-session": "^1.18.1", 41 | "ts-node": "^10.9.2", 42 | "typescript": "^5.8.3", 43 | "vite": "^6.3.5", 44 | "vite-plugin-dts": "^4.5.4", 45 | "vitest": "^3.2.3" 46 | }, 47 | "peerDependencies": { 48 | "redis": ">=5", 49 | "express-session": ">=1" 50 | }, 51 | "engines": { 52 | "node": ">=18" 53 | }, 54 | "bugs": { 55 | "url": "https://github.com/tj/connect-redis/issues" 56 | }, 57 | "keywords": ["connect", "redis", "session", "express"] 58 | } 59 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [](https://github.com/tj/connect-redis/actions/workflows/build.yml) [](https://npmjs.com/package/connect-redis)  2 | 3 | **connect-redis** provides Redis session storage for Express. 4 | 5 | ## Installation 6 | 7 | **connect-redis** requires `express-session` and [`redis`][1]: 8 | 9 | ```sh 10 | npm install redis connect-redis express-session 11 | ``` 12 | 13 | ## API 14 | 15 | Full setup: 16 | 17 | ```js 18 | import {RedisStore} from "connect-redis" 19 | import session from "express-session" 20 | import {createClient} from "redis" 21 | 22 | // Initialize client. 23 | let redisClient = createClient() 24 | redisClient.connect().catch(console.error) 25 | 26 | // Initialize store. 27 | let redisStore = new RedisStore({ 28 | client: redisClient, 29 | prefix: "myapp:", 30 | }) 31 | 32 | // Initialize session storage. 33 | app.use( 34 | session({ 35 | store: redisStore, 36 | resave: false, // required: force lightweight session keep alive (touch) 37 | saveUninitialized: false, // recommended: only save session when data exists 38 | secret: "keyboard cat", 39 | }), 40 | ) 41 | ``` 42 | 43 | ### RedisStore(options) 44 | 45 | #### Options 46 | 47 | ##### client 48 | 49 | An instance of [`redis`][1] 50 | 51 | ##### prefix 52 | 53 | Key prefix in Redis (default: `sess:`). 54 | 55 | **Note**: This prefix appends to whatever prefix you may have set on the `client` itself. 56 | 57 | **Note**: You may need unique prefixes for different applications sharing the same Redis instance. This limits bulk commands exposed in `express-session` (like `length`, `all`, `keys`, and `clear`) to a single application's data. 58 | 59 | ##### ttl 60 | 61 | If the session cookie has a `expires` date, `connect-redis` will use it as the TTL. 62 | 63 | Otherwise, it will expire the session using the `ttl` option (default: `86400` seconds or one day). 64 | 65 | ```ts 66 | interface RedisStoreOptions { 67 | ... 68 | ttl?: number | {(sess: SessionData): number} 69 | } 70 | ``` 71 | 72 | `ttl` also has external callback support. You can use it for dynamic TTL generation. It has access to `session` data. 73 | 74 | **Note**: The TTL is reset every time a user interacts with the server. You can disable this behavior in _some_ instances by using `disableTouch`. 75 | 76 | **Note**: `express-session` does not update `expires` until the end of the request life cycle. _Calling `session.save()` manually beforehand will have the previous value_. 77 | 78 | ##### disableTouch 79 | 80 | Disables resetting the TTL when using `touch` (default: `false`) 81 | 82 | The `express-session` package uses `touch` to signal to the store that the user has interacted with the session but hasn't changed anything in its data. Typically, this helps keep the users session alive if session changes are infrequent but you may want to disable it to cut down the extra calls or to prevent users from keeping sessions open too long. Also consider enabling if you store a lot of data on the session. 83 | 84 | Ref: <https://github.com/expressjs/session#storetouchsid-session-callback> 85 | 86 | ##### disableTTL 87 | 88 | Disables key expiration completely (default: `false`) 89 | 90 | This option disables key expiration requiring the user to manually manage key cleanup outside of `connect-redis`. Only use if you know what you are doing and have an exceptional case where you need to manage your own expiration in Redis. 91 | 92 | **Note**: This has no effect on `express-session` setting cookie expiration. 93 | 94 | ##### serializer 95 | 96 | Provide a custom encoder/decoder to use when storing and retrieving session data from Redis (default: `JSON.parse` and `JSON.stringify`). 97 | 98 | Optionally `parse` method can be async if need be. 99 | 100 | ```ts 101 | interface Serializer { 102 | parse(string): object | Promise<object> 103 | stringify(object): string 104 | } 105 | ``` 106 | 107 | ##### scanCount 108 | 109 | Value used for _count_ parameter in [Redis `SCAN` command](https://redis.io/commands/scan#the-count-option). Used for `ids()` and `all()` methods (default: `100`). 110 | 111 | [1]: https://github.com/NodeRedis/node-redis 112 | -------------------------------------------------------------------------------- /testdata/server.ts: -------------------------------------------------------------------------------- 1 | import {type ChildProcess, spawn} from "node:child_process" 2 | let redisSrv: ChildProcess 3 | 4 | export const port = "18543" 5 | 6 | export function connect() { 7 | return new Promise((resolve, reject) => { 8 | redisSrv = spawn("redis-server", ["--port", port, "--loglevel", "notice"], { 9 | stdio: "inherit", 10 | }) 11 | 12 | redisSrv.on("error", (err) => { 13 | reject(new Error("Error caught spawning the server:" + err.message)) 14 | }) 15 | 16 | setTimeout(resolve, 1500) 17 | }) 18 | } 19 | 20 | export function disconnect() { 21 | redisSrv.kill("SIGKILL") 22 | return Promise.resolve() 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "esnext", 5 | "strict": true, 6 | "isolatedModules": true, 7 | "skipLibCheck": true, 8 | "noImplicitReturns": true, 9 | "noUnusedLocals": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "moduleResolution": "node", 12 | "esModuleInterop": true, 13 | "resolveJsonModule": true 14 | }, 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import {copyFileSync} from "node:fs" 2 | import dts from "vite-plugin-dts" 3 | import {defineConfig} from "vitest/config" 4 | 5 | // https://vitest.dev/config/ 6 | export default defineConfig({ 7 | build: { 8 | lib: { 9 | entry: "index.ts", 10 | name: "connect-redis", 11 | formats: ["es", "cjs"], 12 | }, 13 | emptyOutDir: true, 14 | minify: false, 15 | rollupOptions: { 16 | external: ["express-session"], 17 | treeshake: false, 18 | }, 19 | target: "node18", 20 | }, 21 | plugins: [ 22 | dts({ 23 | include: ["index.ts"], 24 | rollupTypes: true, 25 | insertTypesEntry: true, 26 | afterBuild: () => { 27 | copyFileSync("dist/connect-redis.d.ts", "dist/connect-redis.d.cts") 28 | }, 29 | }), 30 | ], 31 | test: { 32 | include: ["**/*_test.[jt]s"], 33 | coverage: { 34 | reporter: ["text"], 35 | }, 36 | }, 37 | }) 38 | --------------------------------------------------------------------------------