├── .github └── workflows │ ├── ci-and-publish.yml │ ├── ci.yml │ └── codeql-analysis.yml ├── .gitignore ├── .prettierignore ├── .releaserc.json ├── .size-limit.json ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── benchmark └── index.mjs ├── codecov.yml ├── eslint.config.mjs ├── media ├── banner.jpg ├── perf.png └── size.png ├── package-lock.json ├── package.json ├── prettier.config.mjs ├── rollup.config.ts ├── src └── index.ts ├── tests └── index.test.ts ├── tsconfig.json └── vitest.config.ts /.github/workflows/ci-and-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: ci-and-publish 5 | 6 | on: 7 | push: 8 | branches: [main, next] 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node-version: [20.x] 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | cache: npm 23 | - run: npm ci 24 | - run: npm run lint 25 | - run: npm test 26 | 27 | publish-npm: 28 | needs: test 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: actions/setup-node@v4 33 | with: 34 | node-version: 20 35 | registry-url: https://registry.npmjs.org/ 36 | cache: npm 37 | - run: npm ci 38 | - run: npm run build 39 | - run: npx semantic-release 40 | env: 41 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 42 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 43 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | branches: [main, next] 6 | 7 | jobs: 8 | build-and-test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node-version: [20.x] 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | cache: npm 20 | - run: npm ci 21 | - run: npm run lint 22 | - run: npm test 23 | - run: npm run build 24 | - name: Upload coverage to Codecov 25 | uses: codecov/codecov-action@v2 26 | size: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: andresz1/size-limit-action@v1 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [main, next] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [main] 9 | schedule: 10 | - cron: "32 10 * * 3" 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: ["javascript"] 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | 30 | - name: Initialize CodeQL 31 | uses: github/codeql-action/init@v1 32 | with: 33 | languages: ${{ matrix.language }} 34 | 35 | - name: Perform CodeQL Analysis 36 | uses: github/codeql-action/analyze@v1 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Builds 5 | dist 6 | types 7 | coverage 8 | 9 | # Misc 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "main", 4 | { "name": "next", "channel": "next", "prerelease": "next" } 5 | ], 6 | "preset": "conventionalcommits", 7 | "plugins": [ 8 | [ 9 | "@semantic-release/commit-analyzer", 10 | { 11 | "releaseRules": [ 12 | { "type": "docs", "release": "patch" }, 13 | { "type": "test", "release": "patch" }, 14 | { "type": "chore", "release": "patch" } 15 | ], 16 | "parserOpts": { 17 | "noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES"] 18 | } 19 | } 20 | ], 21 | [ 22 | "@semantic-release/release-notes-generator", 23 | { 24 | "presetConfig": { 25 | "types": [ 26 | { 27 | "type": "feat", 28 | "section": "Features", 29 | "hidden": false 30 | }, 31 | { 32 | "type": "fix", 33 | "section": "Bug Fixes", 34 | "hidden": false 35 | }, 36 | { 37 | "type": "docs", 38 | "section": "Documentation", 39 | "hidden": false 40 | }, 41 | { 42 | "type": "test", 43 | "section": "Tests", 44 | "hidden": false 45 | }, 46 | { 47 | "type": "chore", 48 | "section": "Chores", 49 | "hidden": false 50 | } 51 | ] 52 | } 53 | } 54 | ], 55 | "@semantic-release/npm", 56 | "@semantic-release/github" 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /.size-limit.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "CJS Minified", 4 | "path": "dist/cjs/*.js", 5 | "gzip": false, 6 | "brotli": false, 7 | "limit": "282B" 8 | }, 9 | { 10 | "name": "CJS Gzipped", 11 | "path": "dist/cjs/*.js", 12 | "gzip": true, 13 | "brotli": false, 14 | "limit": "203B" 15 | }, 16 | { 17 | "name": "CJS Brotlied", 18 | "path": "dist/cjs/*.js", 19 | "gzip": false, 20 | "brotli": true, 21 | "limit": "173B" 22 | }, 23 | { 24 | "name": "ESM Minified", 25 | "path": "dist/esm/*.mjs", 26 | "gzip": false, 27 | "brotli": false, 28 | "limit": "134B" 29 | }, 30 | { 31 | "name": "ESM Gzipped", 32 | "path": "dist/esm/*.mjs", 33 | "gzip": true, 34 | "brotli": false, 35 | "limit": "107B" 36 | }, 37 | { 38 | "name": "ESM Brotlied", 39 | "path": "dist/esm/*.mjs", 40 | "gzip": false, 41 | "brotli": true, 42 | "limit": "90B" 43 | } 44 | ] 45 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Alex Nault 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 | ![Banner](media/banner.jpg) 2 | 3 | # classix 4 | 5 | ![NPM version](https://img.shields.io/npm/v/classix?style=flat-square) 6 | ![Build](https://img.shields.io/github/actions/workflow/status/alexnault/classix/ci-and-publish.yml?branch=main&style=flat-square) 7 | ![Test coverage](https://img.shields.io/codecov/c/github/alexnault/classix?style=flat-square) 8 | ![Monthly downloads](https://img.shields.io/npm/dm/classix?style=flat-square) 9 | ![Size](https://img.shields.io/badge/dynamic/json?color=blue&label=size&query=$.size.uncompressedSize&url=https://deno.bundlejs.com?q=classix&style=flat-square) 10 | 11 | The [fastest](#performance) and [tiniest](#size) utility for conditionally joining classNames. 12 | 13 | ## Installation 14 | 15 | ```bash 16 | npm install classix 17 | ``` 18 | 19 | ## Usage 20 | 21 | Use any amount of string expressions and classix will join them like so: 22 | 23 | ```js 24 | import cx from "classix"; 25 | // or 26 | import { cx } from "classix"; 27 | 28 | cx("class1", "class2"); 29 | // => "class1 class2" 30 | 31 | cx("class1 class2", "class3", "class4 class5"); 32 | // => "class1 class2 class3 class4 class5" 33 | 34 | cx("class1", true && "class2"); 35 | // => "class1 class2" 36 | 37 | cx(false && "class1", "class2"); 38 | // => "class2" 39 | 40 | cx(true ? "class1" : "class2"); 41 | // => "class1" 42 | 43 | cx("class1", false ? "class2" : "class3"); 44 | // => "class1 class3" 45 | 46 | cx(...["class1", "class2", "class3"]); 47 | // => class1 class2 class3 48 | 49 | cx( 50 | "flex", 51 | isPrimary ? "bg-primary-100" : "bg-secondary-100", 52 | isLarge ? "m-4 p-4" : "m-2 p-2", 53 | ); 54 | // => "flex bg-primary-100 m-2 p-2" *assuming isPrimary is true and isLarge is false 55 | ``` 56 | 57 | ## Comparison 58 | 59 | Compared to other libraries, classix only allows string expressions as arguments: 60 | 61 | ```js 62 | // 🚫 63 | clsx({ "class-1": isPrimary }); 64 | // ✅ 65 | cx(isPrimary && "class-1"); 66 | 67 | // 🚫 68 | clsx({ "class-1": isPrimary && isLarge, "class-2": !isPrimary || !isLarge }); 69 | // ✅ 70 | cx(isPrimary && isLarge ? "class-1" : "class-2"); 71 | ``` 72 | 73 | String expressions have a few benefits over objects: 74 | 75 | - A faster typing experience 76 | - A more intuitive syntax (conditions first) 77 | - `else` support through ternary operators 78 | 79 | What's more, by leveraging them, classix provides: 80 | 81 | - A simpler and consistent API 82 | - [A smaller library size](#size) 83 | - [Better performance](#performance) 84 | 85 | ### Size 86 | 87 | ![Size comparison chart](media/size.png) 88 | 89 | Sources: [classix](https://deno.bundlejs.com?q=classix), [clsx](https://deno.bundlejs.com?q=clsx), [classnames](https://deno.bundlejs.com?q=classnames) 90 | 91 | ### Performance 92 | 93 | ![Performance comparison chart](media/perf.png) 94 | 95 | Sources: Ran [benchmark](benchmark/) on an AMD Ryzen 5 5600x with Node 20. 96 | 97 | ## Highlights 98 | 99 | - Supports all major browsers 100 | - Supports all versions of Node.js 101 | - Works with both ES Modules and CommonJS 102 | - Zero dependencies 103 | - Fully typed with TypeScript 104 | - Fully tested 105 | - [Semver](https://semver.org/) compliant 106 | 107 | ## Migrating to classix 108 | 109 | If you are using `classnames` or `clsx`, you can migrate to `classix` by changing your `imports`: 110 | 111 | ```diff 112 | - import classnames from 'classnames'; 113 | + import cx from 'classix'; 114 | ``` 115 | 116 | And if you were using object arguments, you'll have to convert them to string arguments: 117 | 118 | ```diff 119 | - classnames({ 'class-1': cond1, 'class-2': cond2 }); 120 | + cx(cond1 && 'class-1', cond2 && 'class-2') 121 | ``` 122 | 123 | That's it! 124 | 125 | ## Some love ❤️ 126 | 127 | > "This looks great. I agree that the object notation is not great and not worth the perf hit." — @jmeistrich 128 | 129 | > "It looks good! I like the idea that you can’t pass objects and is simple and minimal. I will use it on my next application instead of classnames." — @m0ment98 130 | 131 | > "Thank you for creating and maintaining this package! It is great." — @charkour 132 | 133 | ## Changelog 134 | 135 | For a list of changes and releases, see the [changelog](https://github.com/alexnault/classix/releases). 136 | 137 | ## Contributing 138 | 139 | Found a bug, have a question or looking to improve classix? Open an [issue](https://github.com/alexnault/classix/issues/new), start a [discussion](https://github.com/alexnault/classix/discussions/new) or submit a [PR](https://github.com/alexnault/classix/fork)! 140 | -------------------------------------------------------------------------------- /benchmark/index.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-constant-binary-expression */ 2 | import { performance } from "node:perf_hooks"; 3 | import classnames from "classnames"; 4 | import { clsx } from "clsx"; 5 | import { cx } from "classix"; 6 | 7 | import { cx as cxLocal } from "../dist/esm/classix.mjs"; 8 | 9 | const NB_RUN = 100_000_000; 10 | 11 | function logResult(name, ms) { 12 | const millionsOfOpsPerSecond = ((NB_RUN * 1000) / ms / 1_000_000).toFixed(1); 13 | console.log(`${name}: ${millionsOfOpsPerSecond}M ops/s`); 14 | } 15 | 16 | // Repetition of code was used as to not affect performance 17 | let start = 0; 18 | let stop = 0; 19 | 20 | start = performance.now(); 21 | for (let i = 0; i < NB_RUN; i++) { 22 | classnames("class-1", { "class-2": true }); 23 | } 24 | stop = performance.now(); 25 | logResult("classnames (object)", stop - start); 26 | 27 | // 28 | 29 | start = performance.now(); 30 | for (let i = 0; i < NB_RUN; i++) { 31 | classnames("class-1", true && "class-2"); 32 | } 33 | stop = performance.now(); 34 | logResult("classnames", stop - start); 35 | 36 | // 37 | 38 | start = performance.now(); 39 | for (let i = 0; i < NB_RUN; i++) { 40 | clsx("class-1", { "class-2": true }); 41 | } 42 | stop = performance.now(); 43 | logResult("clsx (object)", stop - start); 44 | 45 | // 46 | 47 | start = performance.now(); 48 | for (let i = 0; i < NB_RUN; i++) { 49 | clsx("class-1", true && "class-2"); 50 | } 51 | stop = performance.now(); 52 | logResult("clsx", stop - start); 53 | 54 | // 55 | 56 | start = performance.now(); 57 | for (let i = 0; i < NB_RUN; i++) { 58 | cx("class-1", true && "class-2"); 59 | } 60 | stop = performance.now(); 61 | logResult("classix", stop - start); 62 | 63 | // 64 | 65 | start = performance.now(); 66 | for (let i = 0; i < NB_RUN; i++) { 67 | cxLocal("class-1", true && "class-2"); 68 | } 69 | stop = performance.now(); 70 | logResult("classix (local)", stop - start); 71 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: 100% 6 | threshold: 0% 7 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from "@eslint/js"; 4 | import tseslint from "typescript-eslint"; 5 | import parser from "@typescript-eslint/parser"; 6 | import plugin from "@typescript-eslint/eslint-plugin"; 7 | import eslintConfigPrettier from "eslint-config-prettier"; 8 | import globals from "globals"; 9 | 10 | export default [ 11 | eslint.configs.recommended, 12 | ...tseslint.configs.recommended, 13 | eslintConfigPrettier, 14 | { 15 | // https://github.com/eslint/eslint/issues/17930#issuecomment-1872947672 16 | ignores: ["node_modules/*", "dist/*", "coverage/*"], 17 | }, 18 | { 19 | languageOptions: { 20 | parser: parser, 21 | globals: { 22 | ...globals.browser, 23 | ...globals.node, 24 | }, 25 | }, 26 | plugins: { 27 | "@typescript-eslint": plugin, 28 | }, 29 | rules: {}, 30 | ignores: ["node_modules/*", "dist/*", "coverage/*"], 31 | }, 32 | ]; 33 | -------------------------------------------------------------------------------- /media/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexnault/classix/0961da612e640f67473205386378e62487f5c352/media/banner.jpg -------------------------------------------------------------------------------- /media/perf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexnault/classix/0961da612e640f67473205386378e62487f5c352/media/perf.png -------------------------------------------------------------------------------- /media/size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexnault/classix/0961da612e640f67473205386378e62487f5c352/media/size.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "classix", 3 | "version": "1.0.0-semantic-release", 4 | "description": "The fastest and tiniest utility for conditionally joining classNames.", 5 | "main": "./dist/cjs/classix.js", 6 | "module": "./dist/esm/classix.mjs", 7 | "types": "./dist/classix.d.ts", 8 | "exports": { 9 | ".": { 10 | "types": "./dist/classix.d.ts", 11 | "require": "./dist/cjs/classix.js", 12 | "import": "./dist/esm/classix.mjs" 13 | }, 14 | "./package.json": "./package.json" 15 | }, 16 | "author": "Alex Nault", 17 | "keywords": [ 18 | "class", 19 | "classes", 20 | "classname", 21 | "classnames", 22 | "clsx", 23 | "tailwind", 24 | "css" 25 | ], 26 | "license": "MIT", 27 | "repository": "https://github.com/alexnault/classix", 28 | "homepage": "https://github.com/alexnault/classix#readme", 29 | "scripts": { 30 | "bench": "node benchmark/index.mjs", 31 | "build": "rm -rf ./dist && rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript", 32 | "format": "prettier ./ --write", 33 | "lint": "eslint ./ --max-warnings=0", 34 | "size": "size-limit", 35 | "test": "vitest run --coverage" 36 | }, 37 | "files": [ 38 | "dist" 39 | ], 40 | "sideEffects": false, 41 | "devDependencies": { 42 | "@eslint/js": "^9.11.1", 43 | "@rollup/plugin-terser": "^0.4.4", 44 | "@rollup/plugin-typescript": "^12.1.0", 45 | "@size-limit/preset-small-lib": "^11.1.5", 46 | "@types/eslint__js": "^8.42.3", 47 | "@typescript-eslint/eslint-plugin": "^8.7.0", 48 | "@typescript-eslint/parser": "^8.7.0", 49 | "@vitest/coverage-v8": "^2.1.1", 50 | "classix": "^2.1.31", 51 | "classnames": "^2.5.1", 52 | "clsx": "^2.1.1", 53 | "conventional-changelog-conventionalcommits": "^8.0.0", 54 | "esbuild": "^0.25.0", 55 | "eslint": "^9.11.1", 56 | "eslint-config-prettier": "^9.1.0", 57 | "prettier": "^3.3.3", 58 | "rollup": "^4.22.4", 59 | "rollup-plugin-dts": "^6.1.1", 60 | "rollup-plugin-esbuild": "^6.1.1", 61 | "semantic-release": "^24.1.1", 62 | "size-limit": "^11.1.5", 63 | "typescript": "^5.6.2", 64 | "typescript-eslint": "^8.7.0", 65 | "vitest": "^2.1.1" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://prettier.io/docs/en/configuration.html 3 | * @type {import("prettier").Config} 4 | */ 5 | const config = {}; 6 | 7 | export default config; 8 | -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import dts from "rollup-plugin-dts"; 2 | import esbuild from "rollup-plugin-esbuild"; 3 | import terser from "@rollup/plugin-terser"; 4 | import type { RollupOptions } from "rollup"; 5 | 6 | const config: RollupOptions[] = [ 7 | { 8 | input: "src/index.ts", 9 | plugins: [esbuild(), terser()], 10 | output: [ 11 | { 12 | file: `dist/cjs/classix.js`, 13 | format: "cjs", 14 | exports: "named", 15 | strict: false, // Don't emit "use strict" in output 16 | }, 17 | { 18 | file: `dist/esm/classix.mjs`, 19 | format: "es", 20 | }, 21 | ], 22 | }, 23 | { 24 | input: "src/index.ts", 25 | plugins: [dts()], 26 | output: { 27 | file: `dist/classix.d.ts`, 28 | format: "es", 29 | }, 30 | }, 31 | ]; 32 | 33 | export default config; 34 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export type ClassName = string | boolean | null | undefined; 2 | 3 | /** 4 | * Conditionally join classNames into a single string 5 | * @param {...String} args The expressions to evaluate 6 | * @returns {String} The joined classNames 7 | */ 8 | function cx(...args: ClassName[]): string; 9 | function cx(): string { 10 | let str = "", 11 | i = 0, 12 | arg: unknown; 13 | 14 | for (; i < arguments.length; ) { 15 | // eslint-disable-next-line prefer-rest-params 16 | if ((arg = arguments[i++]) && typeof arg === "string") { 17 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions 18 | str && (str += " "); 19 | str += arg; 20 | } 21 | } 22 | return str; 23 | } 24 | 25 | export { cx }; 26 | export default cx; 27 | -------------------------------------------------------------------------------- /tests/index.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-constant-condition */ 2 | /* eslint-disable no-constant-binary-expression */ 3 | import { describe, it, expect } from "vitest"; 4 | 5 | import { cx } from "../src"; 6 | 7 | describe("cx", () => { 8 | it("undefined", () => { 9 | expect(cx()).toBe(""); 10 | expect(cx(undefined)).toBe(""); 11 | expect(cx(undefined, "foo")).toBe("foo"); 12 | }); 13 | 14 | it("null", () => { 15 | expect(cx(null)).toBe(""); 16 | expect(cx(null, "foo")).toBe("foo"); 17 | }); 18 | 19 | it("string", () => { 20 | expect(cx("")).toBe(""); 21 | expect(cx("foo")).toBe("foo"); 22 | expect(cx("foo bar")).toBe("foo bar"); 23 | expect(cx("foo", "bar")).toBe("foo bar"); 24 | expect(cx("foo bar", "foo2 bar2")).toBe("foo bar foo2 bar2"); 25 | expect(cx("foo", "", "bar")).toBe("foo bar"); 26 | }); 27 | 28 | it("boolean", () => { 29 | expect(cx(true)).toBe(""); 30 | expect(cx(false)).toBe(""); 31 | 32 | expect(cx(true && "foo")).toBe("foo"); 33 | expect(cx(false && "foo")).toBe(""); 34 | 35 | expect(cx("foo", true && "bar")).toBe("foo bar"); 36 | expect(cx("foo", false && "bar")).toBe("foo"); 37 | 38 | expect(cx(true ? "foo" : "bar")).toBe("foo"); 39 | expect(cx(false ? "foo" : "bar")).toBe("bar"); 40 | 41 | expect(cx("foo", true ? "bar1" : "bar2")).toBe("foo bar1"); 42 | expect(cx("foo", false ? "bar1" : "bar2")).toBe("foo bar2"); 43 | 44 | expect(cx("0")).toBe("0"); 45 | expect(cx("7")).toBe("7"); 46 | }); 47 | 48 | it("number", () => { 49 | // @ts-expect-error Testing outside of types 50 | expect(cx(0)).toBe(""); 51 | // @ts-expect-error Testing outside of types 52 | expect(cx(7)).toBe(""); 53 | // @ts-expect-error Testing outside of types 54 | expect(cx(-7)).toBe(""); 55 | // @ts-expect-error Testing outside of types 56 | expect(cx(-0)).toBe(""); 57 | // @ts-expect-error Testing outside of types 58 | expect(cx(1_000_000)).toBe(""); 59 | // @ts-expect-error Testing outside of types 60 | expect(cx(1.5)).toBe(""); 61 | // @ts-expect-error Testing outside of types 62 | expect(cx(333e9)).toBe(""); 63 | // @ts-expect-error Testing outside of types 64 | expect(cx(Infinity)).toBe(""); 65 | }); 66 | 67 | it("object", () => { 68 | // @ts-expect-error Testing outside of types 69 | expect(cx({})).toBe(""); 70 | // @ts-expect-error Testing outside of types 71 | expect(cx({ foo: "bar" })).toBe(""); 72 | }); 73 | 74 | it("array", () => { 75 | expect(cx(...["foo", "bar"])).toBe("foo bar"); 76 | // @ts-expect-error Testing outside of types 77 | expect(cx([])).toBe(""); 78 | // @ts-expect-error Testing outside of types 79 | expect(cx(["foo"])).toBe(""); 80 | // @ts-expect-error Testing outside of types 81 | expect(cx([[["foo"]]])).toBe(""); 82 | }); 83 | 84 | it("function", () => { 85 | // @ts-expect-error Testing outside of types 86 | expect(cx(() => "")).toBe(""); 87 | // @ts-expect-error Testing outside of types 88 | expect(cx(() => "foo")).toBe(""); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "isolatedModules": false, 6 | "lib": ["esnext"], 7 | "module": "ESNext", 8 | "moduleResolution": "node", 9 | "noEmit": true, 10 | "noImplicitAny": true, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "outDir": "dist", 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "target": "esnext" 19 | }, 20 | "include": ["src/**/*", "rollup.config.ts"] 21 | } 22 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | coverage: { 6 | provider: "v8", 7 | reporter: ["lcov", "text"], 8 | thresholds: { 9 | branches: 100, 10 | functions: 100, 11 | lines: 100, 12 | statements: 100, 13 | }, 14 | include: ["src"], 15 | }, 16 | }, 17 | }); 18 | --------------------------------------------------------------------------------