From 1a491425ef048efbe07ccea2a80a308e9870926c Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Fri, 29 Dec 2023 12:30:13 -0800 Subject: [PATCH] feat: add `clsx/lite` module --- bench/index.js | 2 ++ bench/readme.md | 1 + bin/index.js | 65 +++++++++++++++++++++++++++++++++++++------------ package.json | 28 +++++++++++++++------ readme.md | 47 ++++++++++++++++++++++++++++++++++- src/lite.js | 13 ++++++++++ test/lite.js | 63 +++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 194 insertions(+), 25 deletions(-) create mode 100644 src/lite.js create mode 100644 test/lite.js diff --git a/bench/index.js b/bench/index.js index 1b18a40..0f4275b 100644 --- a/bench/index.js +++ b/bench/index.js @@ -3,6 +3,7 @@ const classnames = require('classnames'); const classcat = require('classcat'); const clsx = require('../dist/clsx'); const old = require('clsx'); +const lite = require('../dist/lite'); function bench(name, ...args) { console.log(`\n# ${name}`); @@ -11,6 +12,7 @@ function bench(name, ...args) { .add('classnames ', () => classnames.apply(classnames, args)) .add('clsx (prev) ', () => old.apply(old, args)) .add('clsx ', () => clsx.apply(clsx, args)) + .add('clsx (lite) ', () => lite.apply(lite, args)) .on('cycle', e => console.log(' ' + e.target)) .run(); } diff --git a/bench/readme.md b/bench/readme.md index a3d84e4..77e69a1 100644 --- a/bench/readme.md +++ b/bench/readme.md @@ -13,6 +13,7 @@ These are the results while running this directory's benchmark suite in Node v20 classcat ≠ x 9,613,381 ops/sec ±0.16% (94 runs sampled) classnames x 6,540,072 ops/sec ±0.11% (101 runs sampled) clsx x 12,924,662 ops/sec ±0.15% (102 runs sampled) + clsx/lite x 13,122,004 ops/sec ±0.40% (99 runs sampled) # Objects classcat ≠ x 8,936,903 ops/sec ±0.12% (100 runs sampled) diff --git a/bin/index.js b/bin/index.js index 35530f9..c5abd5d 100644 --- a/bin/index.js +++ b/bin/index.js @@ -4,8 +4,6 @@ const zlib = require('zlib'); const { minify } = require('terser'); const pkg = require('../package.json'); -if (!fs.existsSync('dist')) fs.mkdirSync('dist'); - /** * @param {string} file * @param {string} source @@ -17,22 +15,57 @@ function write(file, source) { compress: true, }); - fs.writeFileSync(file, result.code); - console.log('~> "%s" (%d b)', file, zlib.gzipSync(result.code).byteLength); + if (result.code) { + fs.writeFileSync(file, result.code); + let size = zlib.gzipSync(result.code).byteLength; + console.log('~> "%s" (%d b)', file, size); + } else { + console.error('!! "%s" ::', file, result.error); + } } -let input = fs.readFileSync('src/index.js', 'utf8'); +/** + * @typedef Export + * @property {Condition} import + * @property {Condition} default + */ -// copy for ESM -write(pkg.module, input); +/** + * @typedef Condition + * @property {string} types + * @property {string} default + */ -// transform ESM -> CJS exports -write(pkg.main, input.replace('export function', 'function').replace( - 'export default clsx;', - 'module.exports = clsx;\n' - + 'module.exports.clsx = clsx;' -)); +/** + * @param {string} file + * @param {"." | "./lite"} entry + */ +function bundle(file, entry) { + fs.existsSync('dist') || fs.mkdirSync('dist'); + + /** + * @type {Export} + */ + let output = pkg.exports[entry]; + let input = fs.readFileSync(file, 'utf8'); + + // copy for ESM file + write(output.import.default, input); + + // transform ESM -> CJS exports + write(output.default.default, input.replace('export function', 'function').replace( + 'export default clsx;', + 'module.exports = clsx;\n' + + 'module.exports.clsx = clsx;' + )); + + if (entry === '.') { + // transform ESM -> UMD exports + input = input.replace('export function', 'function').replace('export default clsx;', 'return clsx.clsx=clsx, clsx;'); + write(pkg.unpkg, '!function(global,factory){"object"==typeof exports&&"undefined"!=typeof module?module.exports=factory():"function"==typeof define&&define.amd?define(factory):global.clsx=factory()}(this,function(){' + input + '});'); + } +} -// transform ESM -> UMD exports -input = input.replace('export function', 'function').replace('export default clsx;', 'return clsx.clsx=clsx, clsx;'); -write(pkg.unpkg, '!function(global,factory){"object"==typeof exports&&"undefined"!=typeof module?module.exports=factory():"function"==typeof define&&define.amd?define(factory):global.clsx=factory()}(this,function(){' + input + '});'); +bundle('src/index.js', '.'); +console.log('---'); +bundle('src/lite.js', './lite'); diff --git a/package.json b/package.json index 26ee461..4df4ae5 100644 --- a/package.json +++ b/package.json @@ -6,18 +6,30 @@ "module": "dist/clsx.mjs", "unpkg": "dist/clsx.min.js", "main": "dist/clsx.js", + "types": "clsx.d.ts", + "license": "MIT", "exports": { - "import": { - "types": "./clsx.d.mts", - "default": "./dist/clsx.mjs" + ".": { + "import": { + "types": "./clsx.d.mts", + "default": "./dist/clsx.mjs" + }, + "default": { + "types": "./clsx.d.ts", + "default": "./dist/clsx.js" + } }, - "default": { - "types": "./clsx.d.ts", - "default": "./dist/clsx.js" + "./lite": { + "import": { + "types": "./clsx.d.mts", + "default": "./dist/lite.mjs" + }, + "default": { + "types": "./clsx.d.ts", + "default": "./dist/lite.js" + } } }, - "types": "clsx.d.ts", - "license": "MIT", "author": { "name": "Luke Edwards", "email": "luke.edwards05@gmail.com", diff --git a/readme.md b/readme.md index dbb69e1..3ba0c73 100644 --- a/readme.md +++ b/readme.md @@ -66,6 +66,45 @@ clsx(true, false, '', null, undefined, 0, NaN); //=> '' ``` +## Modes + +There are multiple "versions" of `clsx` available, which allows you to bring only the functionality you need! + +#### `clsx` +> **Size (gzip):** 239 bytes
+> **Availability:** CommonJS, ES Module, UMD + +The default `clsx` module; see [API](#API) for info. + +```js +import { clsx } from 'clsx'; +// or +import clsx from 'clsx'; +``` + +#### `clsx/lite` +> **Size (gzip):** 140 bytes
+> **Availability:** CommonJS, ES Module
+> **CAUTION:** Accepts **ONLY** string arguments! + +Ideal for applications that ***only*** use the string-builder pattern. + +Any non-string arguments are ignored! + +```js +import { clsx } from 'clsx/lite'; +// or +import clsx from 'clsx/lite'; + +// string +clsx('hello', true && 'foo', false && 'bar'); +// => "hello foo" + +// NOTE: Any non-string input(s) ignored +clsx({ foo: true }); +//=> "" +``` + ## Benchmarks For snapshots of cross-browser results, check out the [`bench`](bench) directory~! @@ -81,8 +120,8 @@ All browsers that support [`Array.isArray`](https://developer.mozilla.org/en-US/ ## Tailwind Support Here some additional (optional) steps to enable classes autocompletion using `clsx` with Tailwind CSS. -
+
Visual Studio Code @@ -100,6 +139,12 @@ Here some additional (optional) steps to enable classes autocompletion using `cl ```
+You may find the [`clsx/lite`](#clsxlite) module useful within Tailwind contexts. This is especially true if/when your application **only** composes classes in this pattern: + +```js +clsx('text-base', props.active && 'text-primary', props.className); +``` + ## Related - [obj-str](https://github.com/lukeed/obj-str) - A smaller (96B) and similiar utility that only works with Objects. diff --git a/src/lite.js b/src/lite.js new file mode 100644 index 0000000..c1e9364 --- /dev/null +++ b/src/lite.js @@ -0,0 +1,13 @@ +export function clsx() { + var i=0, tmp, str='', len=arguments.length; + for (; i < len; i++) { + if (tmp = arguments[i]) { + if (typeof tmp === 'string') { + str += (str && ' ') + tmp; + } + } + } + return str; +} + +export default clsx; diff --git a/test/lite.js b/test/lite.js new file mode 100644 index 0000000..157962b --- /dev/null +++ b/test/lite.js @@ -0,0 +1,63 @@ +// @ts-check +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; +import * as mod from '../src/lite'; + +const fn = mod.default; + +test('exports', () => { + assert.type(mod.default, 'function', 'exports default function'); + assert.type(mod.clsx, 'function', 'exports named function'); + assert.is(mod.default, mod.clsx, 'exports are equal'); + + assert.type(mod.default(), 'string', '~> returns string output'); + assert.type(mod.clsx(), 'string', '~> returns string output'); +}); + +test('strings', () => { + assert.is(fn(''), ''); + assert.is(fn('foo'), 'foo'); + assert.is(fn(true && 'foo'), 'foo'); + assert.is(fn(false && 'foo'), ''); +}); + +test('strings (variadic)', () => { + assert.is(fn(''), ''); + assert.is(fn('foo', 'bar'), 'foo bar'); + assert.is(fn(true && 'foo', false && 'bar', 'baz'), 'foo baz'); + assert.is(fn(false && 'foo', 'bar', 'baz', ''), 'bar baz'); +}); + +test('emptys', () => { + assert.is(fn(''), ''); + assert.is(fn(undefined), ''); + assert.is(fn(null), ''); + assert.is(fn(0), ''); +}); + +// lite ignores all non-strings +test('non-strings', () => { + // number + assert.is(fn(1), ''); + assert.is(fn(1, 2), ''); + assert.is(fn(Infinity), ''); + assert.is(fn(NaN), ''); + assert.is(fn(0), ''); + + // objects + assert.is(fn({}), ''); + assert.is(fn(null), ''); + assert.is(fn({ a:1 }), ''); + assert.is(fn({ a:1 }, { b:2 }), ''); + + // arrays + assert.is(fn([]), ''); + assert.is(fn(['foo']), ''); + assert.is(fn(['foo', 'bar']), ''); + + // functions + assert.is(fn(fn), ''); + assert.is(fn(fn, fn), ''); +}); + +test.run();