├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── babel-plugin-optimize-obj-str ├── index.js ├── package.json └── readme.md ├── index.d.ts ├── license ├── package.json ├── readme.md ├── src └── index.js └── test ├── babel.js ├── fixtures ├── dedupe-properties │ ├── input.mjs │ ├── options.json │ └── output.mjs ├── ignore-no-import │ ├── input.mjs │ ├── options.json │ └── output.mjs ├── ignore-unoptimizable │ ├── input.mjs │ ├── options.json │ └── output.mjs ├── optimize │ ├── input.mjs │ ├── options.json │ └── output.mjs ├── strict-ok │ ├── input.mjs │ ├── options.json │ └── output.mjs ├── strict-throw-arg │ ├── input.mjs │ └── options.json ├── strict-throw-multiple-args │ ├── input.mjs │ └── options.json ├── strict-throw-no-arg │ ├── input.mjs │ └── options.json └── strict-throw-property │ ├── input.mjs │ └── options.json └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{json,yml,md}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: lukeed 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Node.js v${{ matrix.nodejs }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | nodejs: [8, 10, 12] 12 | steps: 13 | - uses: actions/checkout@v1 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: ${{ matrix.nodejs }} 17 | 18 | - name: (cache) restore 19 | uses: actions/cache@master 20 | with: 21 | path: node_modules 22 | key: ${{ runner.os }}-${{ hashFiles('**/package.json') }} 23 | 24 | - name: Install 25 | run: npm install 26 | 27 | - name: (coverage) Install 28 | if: matrix.nodejs >= 12 29 | run: npm install -g nyc 30 | 31 | - name: Test 32 | run: npm test 33 | if: matrix.nodejs < 12 34 | 35 | - name: (coverage) Test 36 | run: nyc --include=src --include=babel-plugin-optimize-obj-str npm test 37 | if: matrix.nodejs >= 12 38 | 39 | - name: (coverage) Report 40 | if: matrix.nodejs >= 12 41 | run: | 42 | nyc report --reporter=text-lcov > coverage.lcov 43 | bash <(curl -s https://codecov.io/bash) 44 | env: 45 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *-lock.* 4 | *.lock 5 | *.log 6 | 7 | dist 8 | -------------------------------------------------------------------------------- /babel-plugin-optimize-obj-str/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @typedef {import('@babel/core').Node} Node 5 | * @typedef {import('@babel/core').NodePath<*>} NodePath 6 | * @typedef {import('@babel/core').PluginItem} PluginItem 7 | * @typedef {import('@babel/core').types.ObjectExpression['properties']} ObjectProperties 8 | */ 9 | 10 | /** 11 | * @param {import('@babel/core')} babel 12 | * @param {object} [options] 13 | * @param {boolean} [options.strict] 14 | * @returns {PluginItem} 15 | */ 16 | module.exports = function (babel, options={}) { 17 | const { types: t } = babel; 18 | const { ast } = babel.template.expression; 19 | 20 | /** 21 | * Fails on strict mode when encountering an unoptimizable case. 22 | * @param {NodePath} path 23 | * @param {string} message 24 | */ 25 | function unoptimizable(path, message) { 26 | if (options.strict) throw path.buildCodeFrameError(`${path.node.callee.name}() ` + message); 27 | } 28 | 29 | /** 30 | * Decontextualizes a node for comparison with a different node, irrespective 31 | * of its location in source or surrounding comments. 32 | * @param {Node} node 33 | */ 34 | function decontextualize(node) { 35 | const clean = { ...node }; 36 | delete clean.extra; 37 | delete clean.loc; 38 | delete clean.start; 39 | delete clean.end; 40 | delete clean.range; 41 | delete clean.innerComments; 42 | delete clean.trailingComments; 43 | delete clean.leadingComments; 44 | for (const k in clean) { 45 | if (clean[k] && clean[k].type) { 46 | clean[k] = decontextualize(clean[k]); 47 | } 48 | } 49 | return clean; 50 | } 51 | 52 | /** 53 | * Converts identifier property keys into string literals as mapped by spec, 54 | * as how {a: x} is the same as {'a': x}. 55 | * @NOTE Ignores `SpreadElement` intentionally; see `dedupe` 56 | * @param {ObjectProperties[number]} prop 57 | */ 58 | function propKey(prop) { 59 | return t.isSpreadElement(prop) ? void 0 : 60 | t.isIdentifier(prop.key) && !prop.computed 61 | ? t.stringLiteral(prop.key.name) 62 | : prop.key; 63 | } 64 | 65 | /** 66 | * Removes properties with duplicate keys, honoring the lastly defined value. 67 | * @param {NodePath} path 68 | * @param {ObjectProperties} properties 69 | * @returns {ObjectProperties|void} 70 | */ 71 | function dedupe(path, properties) { 72 | const cache = Object.create(null); 73 | for (let prop of properties) { 74 | if ('key' in prop) { 75 | cache[JSON.stringify(decontextualize(propKey(prop)))] = prop; 76 | } else { 77 | let { type, argument } = prop; 78 | return unoptimizable(path, `must only contain keyed props, found [${type}] ${ 79 | (argument && argument.name) || '(unknown)' 80 | }`); 81 | } 82 | } 83 | return Object.values(cache); 84 | } 85 | 86 | /** 87 | * Replaces a path with a simpler constant value if possible. 88 | * @param {NodePath} path 89 | */ 90 | function tryEval(path) { 91 | const { confident, value } = path.evaluate(); 92 | if (confident) path.replaceWith(ast(JSON.stringify(value))); 93 | } 94 | 95 | /** 96 | * Generates expression to concatenate strings. 97 | * @param {ObjectProperties} properties 98 | */ 99 | function expr(properties) { 100 | return properties.reduce((previous, prop) => { 101 | const condition = prop.value; 102 | const part = propKey(prop); 103 | return previous 104 | ? ast`${previous} + (${condition} ? ' ' + ${part} : '')` 105 | : ast`'' + (${condition} ? ${part} : '')`; 106 | }, null); 107 | } 108 | 109 | return { 110 | name: 'optimize-obj-str', 111 | visitor: { 112 | CallExpression(path) { 113 | const callee = path.get('callee'); 114 | if (!callee.referencesImport('obj-str', 'default')) return; 115 | 116 | const argument = path.node.arguments[0]; 117 | if (path.node.arguments.length !== 1 || !t.isObjectExpression(argument)) { 118 | return unoptimizable(path, 'argument should be a single Object Expression initializer.'); 119 | } 120 | 121 | const properties = dedupe(path, argument.properties); 122 | 123 | if (properties) { 124 | path.replaceWith( 125 | expr(properties) 126 | ); 127 | 128 | path.traverse({ 129 | BinaryExpression: tryEval, 130 | ConditionalExpression: tryEval, 131 | LogicalExpression: tryEval, 132 | }); 133 | 134 | tryEval(path); 135 | } 136 | } 137 | } 138 | }; 139 | }; 140 | -------------------------------------------------------------------------------- /babel-plugin-optimize-obj-str/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "name": "babel-plugin-optimize-objstr", 4 | "description": "Babel plugin to optimize `obj-str` calls.", 5 | "repository": "lukeed/obj-str", 6 | "license": "MIT", 7 | "author": { 8 | "name": "Luke Edwards", 9 | "email": "luke.edwards05@gmail.com", 10 | "url": "https://lukeed.com" 11 | }, 12 | "contributors": [{ 13 | "name": "Alan Orozco", 14 | "email": "alan@orozco.xyz", 15 | "url": "https://alanoroz.co" 16 | }], 17 | "keywords": [ 18 | "babel", 19 | "optimize", 20 | "react", 21 | "preact", 22 | "classes", 23 | "classname", 24 | "classnames", 25 | "object", 26 | "object-keys", 27 | "object-string", 28 | "object-values", 29 | "serialize" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /babel-plugin-optimize-obj-str/readme.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-optimize-obj-str [![CI](https://github.com/lukeed/obj-str/workflows/CI/badge.svg)](https://github.com/lukeed/obj-str/actions) [![codecov](https://badgen.net/codecov/c/github/lukeed/obj-str)](https://codecov.io/gh/lukeed/obj-str) 2 | 3 | > [Babel](https://babeljs.io/) plugin to optimize [`obj-str`](../) calls by replacing them with an equivalent unrolled expression. 4 | 5 | Even though the `obj-str` function is negligible in size over-the-wire, the motivation for this plugin is that transformed expressions [execute almost twice as fast as equivalent calls.](#performance) 6 | 7 | ```js 8 | import objstr from 'obj-str'; 9 | 10 | objstr({ 11 | 'my-classname': true, 12 | 'another-one': maybe, 13 | 'third': a && b, 14 | }); 15 | 16 | // Transformed: 17 | 'my-classname' + (maybe ? ' another-one' : '') + (a && b ? ' third' : ''); 18 | ``` 19 | 20 | ## Install 21 | 22 | ``` 23 | npm install --save-dev babel-plugin-optimize-objstr 24 | ``` 25 | 26 | ## Usage 27 | 28 | **Via babel.config.js** ([recommended](https://babeljs.io/docs/en/configuration)): 29 | 30 | ```js 31 | // babel.config.js 32 | module.exports = { 33 | plugins: ['optimize-objstr'], 34 | }; 35 | ``` 36 | 37 | **Via CLI**: 38 | 39 | ``` 40 | babel --plugins optimize-objstr script.js 41 | ``` 42 | 43 | ## Options 44 | 45 | ```js 46 | // babel.config.js 47 | module.exports = { 48 | plugins: [ 49 | ['optimize-objstr', { 50 | strict: false, 51 | }], 52 | ], 53 | }; 54 | ``` 55 | 56 | ### options.strict 57 | 58 | Type: `boolean`
59 | Default: `false` 60 | 61 | Allow Babel to throw errors when encountering `obj-str` calls that cannot be optimized. 62 | 63 | We can only optimize function calls whose single argument is an **object literal** containing only **keyed properties**. 64 | 65 | ```js 66 | objstr({ 67 | 'optimizable': true, 68 | [classes.myClass]: maybe, 69 | }); 70 | 71 | // Transformed: 72 | 'optimizable' + (maybe ? ' ' + classes.myClass : ''); 73 | ``` 74 | 75 | By default, calls that cannot be optimized are preserved. 76 | 77 | ```js 78 | objstr({ optimizable: true }); 79 | objstr(cannotOptimizeIdentifierArg); 80 | objstr({ ...cannotOptimizeSpread }); 81 | 82 | // Transformed: 83 | ('optimizable'); 84 | objstr(cannotOptimizeIdentifierArg); 85 | objstr({ ...cannotOptimizeSpread }); 86 | ``` 87 | 88 | Preserved calls force the resulting bundle to contain the `objstr()` function, which could otherwise be [dead-code-eliminated](https://en.wikipedia.org/wiki/Dead_code_elimination). 89 | 90 | Instead, when setting the option **`{ strict: true }`** the plugin errors out to prevent this. 91 | 92 |
file.js: objstr() argument should be a single Object Expression initializer.
 93 |   1 | objstr({ 'optimizable': true });
 94 | > 2 | objstr(cannotOptimizeIdentifierArg);
 95 |     | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
96 | 97 | 98 | ## Caveats 99 | 100 | ### Performance 101 | 102 | The purpose of this transform is to improve execution performance. [This benchmark results in a ~1.8x speedup](https://jsbench.me/nukl0mvqze/1) on desktop Chrome (88) (`obj-str` calls are about 45% slower). 103 | 104 | You should not expect this transform should to reduce bundle size. Depending on the amount of object properties and plugin configuration, it might actually output slightly _larger_ code. A rough estimate is that [using less than ~100 different conditional properties](https://gist.github.com/alanorozco/6d83ae5af1ab121757fc29cdb5d77f22) should not increase the size of a bundle. 105 | 106 | ### Leading Space 107 | 108 | Direct results from `objstr()` always omit a leading space. This is not the case when using this transform: 109 | 110 | ```js 111 | objstr({ 112 | a: false, 113 | foo: true 114 | }); 115 | 116 | // transformed: 117 | (' foo'); 118 | ``` 119 | 120 | You must ensure that your expression consumers ignore this leading space. A [classname](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) should work just fine. 121 | 122 | ### Inconsistent Duplicates 123 | 124 | Object literals may contain duplicate property names, in which case [the lastly defined value is preserved.](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#Duplicate_property_names) 125 | 126 | ```js 127 | objstr({ 128 | dupe: one, 129 | dupe: two 130 | }); 131 | 132 | // Transformed: 133 | '' + (two ? 'dupe' : ''); 134 | ``` 135 | 136 | The example above is transformed properly since the duplicate property names are literal and constant. The plugin does its best to override duplicates by comparing property name expressions, but it's unable to compare equal computed results whose expressions vary: 137 | 138 | ```js 139 | objstr({ 140 | good: one, 141 | 'good': two, 142 | ['good']: three, 143 | }); 144 | objstr({ 145 | bad: one, 146 | 'bad': two, 147 | ['ba' + 'd']: three, 148 | }); 149 | 150 | // Transformed: 151 | '' + (three ? 'good' : ''); 152 | '' + (two ? 'bad' : '') + (three ? ' bad' : ''); 153 | ``` 154 | 155 | It's therefore recommended to reserve the use of [computed property names](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#computed_property_names) for identifiers to unique values, and to avoid complex expressions. This reduces the likelihood of mismatched dupes. 156 | 157 | ```js 158 | // These computed names are likely ok: 159 | objstr({ [FOO]: true }); 160 | objstr({ [myConstants.BAR]: true }); 161 | 162 | // More complex computed names are at a higher risk of duping: 163 | objstr({ 164 | [FOO + 'bar']: true, 165 | [FOO + possiblyBar]: false, 166 | }); 167 | ``` 168 | 169 | ## License 170 | 171 | MIT 172 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export default function (input: Record): string; 2 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Luke Edwards (lukeed.com) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obj-str", 3 | "version": "1.1.0", 4 | "repository": "lukeed/obj-str", 5 | "description": "A tiny library for serializing Object values to Strings.", 6 | "unpkg": "dist/obj-str.min.js", 7 | "module": "dist/obj-str.mjs", 8 | "main": "dist/obj-str.js", 9 | "types": "index.d.ts", 10 | "umd:name": "objstr", 11 | "license": "MIT", 12 | "author": { 13 | "name": "Luke Edwards", 14 | "email": "luke.edwards05@gmail.com", 15 | "url": "https://lukeed.com" 16 | }, 17 | "engines": { 18 | "node": ">=4" 19 | }, 20 | "scripts": { 21 | "build": "bundt", 22 | "test": "uvu -r esm -i fixtures test" 23 | }, 24 | "files": [ 25 | "*.d.ts", 26 | "dist" 27 | ], 28 | "keywords": [ 29 | "react", 30 | "preact", 31 | "classes", 32 | "classname", 33 | "classnames", 34 | "object", 35 | "object-keys", 36 | "object-string", 37 | "object-values", 38 | "serialize" 39 | ], 40 | "devDependencies": { 41 | "@babel/core": "7.12.17", 42 | "bundt": "1.1.2", 43 | "esm": "3.2.25", 44 | "uvu": "0.5.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # obj-str [![CI](https://github.com/lukeed/obj-str/workflows/CI/badge.svg)](https://github.com/lukeed/obj-str/actions) [![codecov](https://badgen.net/codecov/c/github/lukeed/obj-str)](https://codecov.io/gh/lukeed/obj-str) 2 | 3 | > A tiny (96B) library for serializing Object values to Strings. 4 | 5 | This module's intended use is for converting an Object with CSS class names (as keys) to a space-delimited `className` string. Other modules have similar goals (like [`classnames`](https://npm.im/classnames)), but `obj-str` only does one thing. This is why it's only 100 bytes gzipped! 6 | 7 | _PS: I made this because [Preact 8.0 removed this built-in behavior](https://github.com/developit/preact/commit/b2c85e3f7fa89ebbf242b00f4cab7619641e3a52) and I wanted a quick, drop-in replacement._ 8 | 9 | ## Install 10 | 11 | ``` 12 | $ npm install --save obj-str 13 | ``` 14 | 15 | 16 | ## Usage 17 | 18 | ```js 19 | import objstr from 'obj-str'; 20 | 21 | objstr({ foo:true, bar:false, baz:isTrue() }); 22 | //=> 'foo baz' 23 | ``` 24 | 25 | ### React 26 | 27 | With React (or any of the React-like libraries!), you can take advantage of any `props` or `state` values in order to express conditional classes as an object. 28 | 29 | ```js 30 | import React from 'react'; 31 | import objstr from 'obj-str'; 32 | 33 | const TodoItem = ({ text, isDone, disabled }) => ( 34 |
  • 35 | 36 | 37 |
  • 38 | ); 39 | ``` 40 | 41 | ### Preact 42 | 43 | For simple use, the [React](#react) example will work for Preact too. However, you may also define a custom vNode "polyfill" to automatically handle Objects when used inside `className`. 44 | 45 | > **Note:** For users of Preact 7.1 and below, _you do not need this module_! Your version includes this behavior out of the box! 46 | 47 | ```js 48 | import objstr from 'obj-str'; 49 | import { options } from 'preact'; 50 | 51 | const old = options.vnode; 52 | 53 | options.vnode = vnode => { 54 | const props = vnode.attributes; 55 | if (props != null) { 56 | const k = 'class' in props ? 'class' : 'className'; 57 | if (props[k] && typeof props[k]=='object') { 58 | props[k] = objstr(props[k]); 59 | } 60 | } 61 | old && old(vnode); 62 | } 63 | ``` 64 | 65 | 66 | ## API 67 | 68 | ### objstr(input) 69 | 70 | #### input 71 | 72 | Type: `Object` 73 | 74 | A hashmap of keys & their truthy/falsey values. Booleans are preferred when speed is critically important. 75 | 76 | 77 | ## Related 78 | 79 | - [babel-plugin-optimize-obj-str](./babel-plugin-optimize-obj-str) - Babel plugin to transform `obj-str` calls into optimized expressions. 80 | 81 | - [clsx](https://github.com/lukeed/clsx) - Drop-in replacement for `obj-str` and `classnames` – handles all (and multiple) input types. 82 | 83 | 84 | ## License 85 | 86 | MIT © [Luke Edwards](http://lukeed.com) 87 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export default function (obj) { 2 | var k, cls=''; 3 | for (k in obj) { 4 | if (obj[k]) { 5 | cls && (cls += ' '); 6 | cls += k; 7 | } 8 | } 9 | return cls; 10 | } 11 | -------------------------------------------------------------------------------- /test/babel.js: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { promisify } from 'util'; 3 | import { readFile, readdirSync } from 'fs'; 4 | import * as assert from 'uvu/assert'; 5 | import { test } from 'uvu'; 6 | 7 | // Babel Setup 8 | import * as BABEL from '@babel/core'; 9 | import PLUGIN from '../babel-plugin-optimize-obj-str'; 10 | 11 | function transform(str, options={}) { 12 | const plugins = [PLUGIN(BABEL, options)]; 13 | return BABEL.transformAsync(str, { plugins }).then(r => r.code + '\n'); 14 | } 15 | 16 | const read = promisify(readFile); 17 | const fixtures = join(__dirname, 'fixtures'); 18 | 19 | readdirSync(fixtures).forEach(dir => { 20 | let input = join(fixtures, dir, 'input.mjs'); 21 | let output = join(fixtures, dir, 'output.mjs'); 22 | let options = join(fixtures, dir, 'options.json'); 23 | let { throws, strict=false } = require(options); 24 | 25 | test(dir, async () => { 26 | let before = await read(input, 'utf8'); 27 | 28 | try { 29 | let result = await transform(before, { strict }); 30 | if (throws) assert.unreachable('should have thrown'); 31 | assert.fixture(result, await read(output, 'utf8')); 32 | } catch (err) { 33 | if (throws) assert.match(err.message, throws); 34 | else assert.unreachable('should not have thrown!'); 35 | } 36 | }); 37 | }); 38 | 39 | test.run(); 40 | -------------------------------------------------------------------------------- /test/fixtures/dedupe-properties/input.mjs: -------------------------------------------------------------------------------- 1 | import objstr from 'obj-str'; 2 | 3 | objstr({ 4 | dupe: 0, 5 | a, 6 | dupe: 1, 7 | b, 8 | dupe: final, 9 | c, 10 | }); 11 | 12 | objstr({ 13 | dupe: 0, 14 | 'dupe': 1, 15 | ['dupe']: final, 16 | }); 17 | 18 | objstr({ 19 | [a + b]: 0, 20 | [a + b]: 1, 21 | [a + b]: final, 22 | }); 23 | -------------------------------------------------------------------------------- /test/fixtures/dedupe-properties/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "throws": false 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/dedupe-properties/output.mjs: -------------------------------------------------------------------------------- 1 | import objstr from 'obj-str'; 2 | '' + (final ? "dupe" : '') + (a ? " a" : '') + (b ? " b" : '') + (c ? " c" : ''); 3 | '' + (final ? 'dupe' : ''); 4 | '' + (final ? a + b : ''); 5 | -------------------------------------------------------------------------------- /test/fixtures/ignore-no-import/input.mjs: -------------------------------------------------------------------------------- 1 | objstr({ 'there-is-no-import-for-this-function-call': true }); 2 | -------------------------------------------------------------------------------- /test/fixtures/ignore-no-import/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "throws": false 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/ignore-no-import/output.mjs: -------------------------------------------------------------------------------- 1 | objstr({ 2 | 'there-is-no-import-for-this-function-call': true 3 | }); 4 | -------------------------------------------------------------------------------- /test/fixtures/ignore-unoptimizable/input.mjs: -------------------------------------------------------------------------------- 1 | import objstr from 'obj-str'; 2 | 3 | function optimized() { 4 | objstr({ 'should-optimize-object-literal': true }); 5 | } 6 | 7 | function ignored() { 8 | objstr(cannotOptimizeReference); 9 | objstr('cannot-optimize-invalid-string-use'); 10 | objstr({ 'cannot-optimize-spread': true, ...spread }); 11 | objstr({ 'cannot-optimize': true }, { 'invalid-use-multiple-args': true }); 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/ignore-unoptimizable/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "throws": false 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/ignore-unoptimizable/output.mjs: -------------------------------------------------------------------------------- 1 | import objstr from 'obj-str'; 2 | 3 | function optimized() { 4 | "should-optimize-object-literal"; 5 | } 6 | 7 | function ignored() { 8 | objstr(cannotOptimizeReference); 9 | objstr('cannot-optimize-invalid-string-use'); 10 | objstr({ 11 | 'cannot-optimize-spread': true, 12 | ...spread 13 | }); 14 | objstr({ 15 | 'cannot-optimize': true 16 | }, { 17 | 'invalid-use-multiple-args': true 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /test/fixtures/optimize/input.mjs: -------------------------------------------------------------------------------- 1 | import objstr from 'obj-str'; 2 | 3 | objstr({ keyIsValueId }); 4 | objstr({ 'quoted': x && y }); 5 | objstr({ unquoted: true }); 6 | objstr({ [identifierExpression]: probably }); 7 | objstr({ [compound + expression]: maybe }); 8 | objstr({ [member.expression]: maybeMemberExpression }); 9 | objstr({ 10 | 'constant-true-first': true, 11 | 'anything else': foo, 12 | }); 13 | objstr({ 14 | keyIsValueId, 15 | 'quoted': x && y, 16 | unquoted: true, 17 | [identifierExpression]: probably, 18 | [compound + expression]: maybe, 19 | [member.expression]: maybeMemberExpression, 20 | }); 21 | objstr({ 22 | 'not-nested': a, 23 | [objstr({ nested: b })]: c, 24 | }); 25 | -------------------------------------------------------------------------------- /test/fixtures/optimize/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "throws": false 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/optimize/output.mjs: -------------------------------------------------------------------------------- 1 | import objstr from 'obj-str'; 2 | '' + (keyIsValueId ? "keyIsValueId" : ''); 3 | '' + (x && y ? 'quoted' : ''); 4 | "unquoted"; 5 | '' + (probably ? identifierExpression : ''); 6 | '' + (maybe ? compound + expression : ''); 7 | '' + (maybeMemberExpression ? member.expression : ''); 8 | "constant-true-first" + (foo ? " anything else" : ''); 9 | '' + (keyIsValueId ? "keyIsValueId" : '') + (x && y ? " quoted" : '') + " unquoted" + (probably ? ' ' + identifierExpression : '') + (maybe ? ' ' + (compound + expression) : '') + (maybeMemberExpression ? ' ' + member.expression : ''); 10 | '' + (a ? 'not-nested' : '') + (c ? ' ' + ('' + (b ? "nested" : '')) : ''); 11 | -------------------------------------------------------------------------------- /test/fixtures/strict-ok/input.mjs: -------------------------------------------------------------------------------- 1 | import objstr from 'obj-str'; 2 | 3 | objstr({ 'should-not-throw': true }); 4 | -------------------------------------------------------------------------------- /test/fixtures/strict-ok/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "strict": true, 3 | "throws": false 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/strict-ok/output.mjs: -------------------------------------------------------------------------------- 1 | import objstr from 'obj-str'; 2 | "should-not-throw"; 3 | -------------------------------------------------------------------------------- /test/fixtures/strict-throw-arg/input.mjs: -------------------------------------------------------------------------------- 1 | import objstr from 'obj-str'; 2 | 3 | objstr(throwsNotObject); 4 | -------------------------------------------------------------------------------- /test/fixtures/strict-throw-arg/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "strict": true, 3 | "throws": "Object Expression initializer" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/strict-throw-multiple-args/input.mjs: -------------------------------------------------------------------------------- 1 | import objstr from 'obj-str'; 2 | 3 | objstr( 4 | { thisObjectArgumentIsOkay: true }, 5 | 'but throws since there is more than one arg' 6 | ); 7 | -------------------------------------------------------------------------------- /test/fixtures/strict-throw-multiple-args/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "strict": true, 3 | "throws": "Object Expression initializer" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/strict-throw-no-arg/input.mjs: -------------------------------------------------------------------------------- 1 | import objstr from 'obj-str'; 2 | 3 | // throws because it lacks arguments 4 | objstr(); 5 | -------------------------------------------------------------------------------- /test/fixtures/strict-throw-no-arg/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "strict": true, 3 | "throws": "Object Expression initializer" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/strict-throw-property/input.mjs: -------------------------------------------------------------------------------- 1 | import objstr from 'obj-str'; 2 | 3 | objstr({ 4 | 'keyed-properties-are-okay': true, 5 | ...butSpreadOperatorIsNotOkay, 6 | }); 7 | -------------------------------------------------------------------------------- /test/fixtures/strict-throw-property/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "strict": true, 3 | "throws": "found [SpreadElement] butSpreadOperatorIsNotOkay" 4 | } 5 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import objstr from '../src'; 4 | 5 | function is(input, expect) { 6 | assert.is(objstr(input), expect); 7 | } 8 | 9 | const API = suite('exports'); 10 | 11 | API('should export a function', () => { 12 | assert.type(objstr, 'function'); 13 | }); 14 | 15 | API.run(); 16 | 17 | // --- 18 | 19 | const usage = suite('usage'); 20 | 21 | usage('true -> ""', () => { 22 | is(true, ''); 23 | }); 24 | 25 | usage('false -> ""', () => { 26 | is(false, ''); 27 | }); 28 | 29 | usage('undefined -> ""', () => { 30 | is(undefined, ''); 31 | }); 32 | 33 | usage('null -> ""', () => { 34 | is(null, ''); 35 | }); 36 | 37 | usage('{} -> ""', () => { 38 | is({}, ''); 39 | }); 40 | 41 | usage('[] -> ""', () => { 42 | is([], ''); 43 | }); 44 | 45 | usage('{ foo } -> "foo"', () => { 46 | is({ foo: true }, 'foo'); 47 | is({ foo: 1 }, 'foo'); 48 | }); 49 | 50 | usage('{ foo, bar } -> "foo"', () => { 51 | is({ foo: true, bar: false }, 'foo'); 52 | is({ foo: 1, bar: 0 }, 'foo'); 53 | }); 54 | 55 | usage('{ foo, bar, baz } -> "foo baz"', () => { 56 | is({ foo: 1 === 1, bar: 1 !== 1, baz: 1 !== 2 }, 'foo baz'); 57 | is({ foo: assert, bar: null, baz: Date }, 'foo baz'); 58 | }); 59 | 60 | usage('{ one, two, bad } -> "one two"', () => { 61 | let one=true, two=true, bad=false; 62 | is({ one, two, bad }, 'one two'); 63 | }); 64 | 65 | usage('{ "-foo": x } -> "-foo"', () => { 66 | is({ '-foo': true }, '-foo'); 67 | is({ '-foo': 0, '-foo': 1 }, '-foo'); 68 | }); 69 | 70 | usage('{ [key]: x } -> key', () => { 71 | let key = 'abc'; 72 | is({ [key]: true }, 'abc'); 73 | }); 74 | 75 | usage.run(); 76 | --------------------------------------------------------------------------------