From df324ac0796270a0f79e125f20c3a17ef037a40d Mon Sep 17 00:00:00 2001 From: Trey Griffith Date: Fri, 21 Aug 2020 00:17:52 -0700 Subject: [PATCH 1/7] add plugins for processing selectors and rules --- src/get-css.ts | 32 +++++++++++++++++++++----------- src/index.tsx | 1 + src/plugins.ts | 28 ++++++++++++++++++++++++++++ test/get-css.ts | 26 ++++++++++++++++++++++++++ test/plugins.ts | 30 ++++++++++++++++++++++++++++++ 5 files changed, 106 insertions(+), 11 deletions(-) create mode 100644 src/plugins.ts create mode 100644 test/plugins.ts diff --git a/src/get-css.ts b/src/get-css.ts index 7f397c7..f7c8443 100644 --- a/src/get-css.ts +++ b/src/get-css.ts @@ -2,6 +2,7 @@ import prefixer, {Rule} from './prefixer' import valueToString from './value-to-string' import getClassName, {PropertyInfo} from './get-class-name' import { EnhancedProp } from './types/enhancers' +import { apply as applyPlugins, RuleSet } from './plugins' /** * Generates the class name and styles. @@ -33,21 +34,30 @@ export default function getCss(propertyInfo: PropertyInfo, value: string | numbe rules = [{property: propertyInfo.cssName || '', value: valueString}] } - let styles: string + const ruleSet = applyPlugins({ + selector: `.${className}`, + rules + }) + + const styles = stringifyRuleSet(ruleSet) + + return {className, styles} +} + +function stringifyRuleSet({ selector, rules }: RuleSet): string { if (process.env.NODE_ENV === 'production') { const rulesString = rules .map(rule => `${rule.property}:${rule.value}`) .join(';') - styles = `.${className}{${rulesString}}` - } else { - const rulesString = rules - .map(rule => ` ${rule.property}: ${rule.value};`) - .join('\n') - styles = ` -.${className} { -${rulesString} -}` + return `${selector}{${rulesString}}` } - return {className, styles} + const rulesString = rules + .map(rule => ` ${rule.property}: ${rule.value};`) + .join('\n') + + return ` +${selector} { +${rulesString} +}` } diff --git a/src/index.tsx b/src/index.tsx index 6040406..dba9d04 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,6 +7,7 @@ export { default as splitBoxProps } from './utils/split-box-props' export { setClassNamePrefix } from './get-class-name' export { configureSafeHref } from './utils/safeHref' export { BoxProps, BoxOwnProps, EnhancerProps, PropsOf, PolymorphicBoxProps, BoxComponent } from './types/box-types' +export { use as usePlugin } from './plugins' export { background, diff --git a/src/plugins.ts b/src/plugins.ts new file mode 100644 index 0000000..122165a --- /dev/null +++ b/src/plugins.ts @@ -0,0 +1,28 @@ +import { Rule } from './prefixer' + +export interface RuleSet { + selector: string, + rules: Rule[] +} + +type Plugin = (set: RuleSet) => RuleSet + +const plugins: Plugin[] = [] + +export function use(plugin: Plugin): void { + plugins.unshift(plugin) +} + +export function apply(set: RuleSet) { + let newSet = {...set} + + for (const plugin of plugins) { + newSet = plugin({...newSet}) + } + + return newSet +} + +export function reset(): void { + plugins.length = 0 +} diff --git a/test/get-css.ts b/test/get-css.ts index e0b1e4a..3dc17a3 100644 --- a/test/get-css.ts +++ b/test/get-css.ts @@ -1,9 +1,11 @@ import test from 'ava' import getCss from '../src/get-css' +import * as plugins from '../src/plugins' const originalNodeEnv = process.env.NODE_ENV test.afterEach.always(() => { process.env.NODE_ENV = originalNodeEnv + plugins.reset() }) test('supports basic prop + value', t => { @@ -59,6 +61,30 @@ test('adds prefixes', t => { ) }) +test('applies plugins', t => { + const propInfo = { + className: 'min-w', + cssName: 'min-width', + jsName: 'minWidth' + } + + plugins.use(({ selector, rules }) => { + return { + selector: `#my-div ${selector}`, + rules + } + }) + + const result = getCss(propInfo, '10px') + t.deepEqual(result, { + className: 'ub-min-w_10px', + styles: ` +#my-div .ub-min-w_10px { + min-width: 10px; +}` + }) +}) + test.serial('returns minified css in production', t => { process.env.NODE_ENV = 'production' const propInfo = { diff --git a/test/plugins.ts b/test/plugins.ts new file mode 100644 index 0000000..2d148ec --- /dev/null +++ b/test/plugins.ts @@ -0,0 +1,30 @@ +import test from 'ava' +import * as plugins from '../src/plugins' + +test.afterEach.always(() => { + plugins.reset() +}) + +test('applies a selector prefix to the styles', t => { + const ruleset = { + selector: '.ub-min-w_10px', + rules: [ + { property: 'min-width', value: '10px' }, + { property: 'user-select', value: 'none' } + ] + } + plugins.use(({ selector, rules }) => { + return { + selector: `#my-div ${selector}`, + rules + } + }) + const result = plugins.apply(ruleset) + t.deepEqual(result, { + selector: '#my-div .ub-min-w_10px', + rules: [ + { property: 'min-width', value: '10px' }, + { property: 'user-select', value: 'none' } + ] + }) +}) From 763bf94ab723dab27d301b7b5f9607a1641584b7 Mon Sep 17 00:00:00 2001 From: Trey Griffith Date: Mon, 24 Aug 2020 21:39:51 -0700 Subject: [PATCH 2/7] bump minor version to reflect new functionality --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b807c5c..7cfb984 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ui-box", - "version": "4.1.0", + "version": "4.2.0", "description": "Blazing Fast React UI Primitive", "contributors": [ "Jeroen Ransijn (https://twitter.com/jeroen_ransijn)", From da002080ea1c1562a3e14d02553a50f05162b740 Mon Sep 17 00:00:00 2001 From: Trey Griffith Date: Tue, 8 Sep 2020 10:34:52 -0700 Subject: [PATCH 3/7] Revert "bump minor version to reflect new functionality" This reverts commit 763bf94ab723dab27d301b7b5f9607a1641584b7. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7cfb984..b807c5c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ui-box", - "version": "4.2.0", + "version": "4.1.0", "description": "Blazing Fast React UI Primitive", "contributors": [ "Jeroen Ransijn (https://twitter.com/jeroen_ransijn)", From 9226188c869b55c0a43701c66d51c9c73bbab14f Mon Sep 17 00:00:00 2001 From: Trey Griffith Date: Tue, 8 Sep 2020 10:36:55 -0700 Subject: [PATCH 4/7] rename plugins.reset() to plugins.clear() --- src/plugins.ts | 2 +- test/get-css.ts | 2 +- test/plugins.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins.ts b/src/plugins.ts index 122165a..6a4b96a 100644 --- a/src/plugins.ts +++ b/src/plugins.ts @@ -23,6 +23,6 @@ export function apply(set: RuleSet) { return newSet } -export function reset(): void { +export function clear(): void { plugins.length = 0 } diff --git a/test/get-css.ts b/test/get-css.ts index 3dc17a3..f667dbb 100644 --- a/test/get-css.ts +++ b/test/get-css.ts @@ -5,7 +5,7 @@ import * as plugins from '../src/plugins' const originalNodeEnv = process.env.NODE_ENV test.afterEach.always(() => { process.env.NODE_ENV = originalNodeEnv - plugins.reset() + plugins.clear() }) test('supports basic prop + value', t => { diff --git a/test/plugins.ts b/test/plugins.ts index 2d148ec..0142ae5 100644 --- a/test/plugins.ts +++ b/test/plugins.ts @@ -2,7 +2,7 @@ import test from 'ava' import * as plugins from '../src/plugins' test.afterEach.always(() => { - plugins.reset() + plugins.clear() }) test('applies a selector prefix to the styles', t => { From cc63ccdfa7af1384a8f005252bbc749feeed29fe Mon Sep 17 00:00:00 2001 From: Trey Griffith Date: Tue, 8 Sep 2020 10:42:17 -0700 Subject: [PATCH 5/7] add JSDoc to new plugin functions --- src/plugins.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/plugins.ts b/src/plugins.ts index 6a4b96a..281430c 100644 --- a/src/plugins.ts +++ b/src/plugins.ts @@ -9,10 +9,18 @@ type Plugin = (set: RuleSet) => RuleSet const plugins: Plugin[] = [] +/** + * Adds a plugin that ui-box will apply before emitting styles into a stylesheet. + * Plugins are applied in the *opposite* order of insertion (most recent `use` first). + */ export function use(plugin: Plugin): void { plugins.unshift(plugin) } +/** + * Applies the added plugins to a given set of CSS rules. + * Should be used only internally by ui-box, not by plugin authors or consumers. + */ export function apply(set: RuleSet) { let newSet = {...set} @@ -23,6 +31,9 @@ export function apply(set: RuleSet) { return newSet } +/** + * Removes all previously added plugins. + */ export function clear(): void { plugins.length = 0 } From 318e26c5e2c4e6dea7d8b7cd22d564e8228824c4 Mon Sep 17 00:00:00 2001 From: Trey Griffith Date: Tue, 8 Sep 2020 10:43:58 -0700 Subject: [PATCH 6/7] remove extraneous object creation --- src/plugins.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins.ts b/src/plugins.ts index 281430c..0e7d64c 100644 --- a/src/plugins.ts +++ b/src/plugins.ts @@ -22,7 +22,7 @@ export function use(plugin: Plugin): void { * Should be used only internally by ui-box, not by plugin authors or consumers. */ export function apply(set: RuleSet) { - let newSet = {...set} + let newSet = set for (const plugin of plugins) { newSet = plugin({...newSet}) From efcdd8f143f53bb719307a3a7f92f513e25d4d5a Mon Sep 17 00:00:00 2001 From: Trey Griffith Date: Tue, 8 Sep 2020 11:48:53 -0700 Subject: [PATCH 7/7] handle plugin that return obviously invalid rules --- src/plugins.ts | 29 ++++++++++++++- src/prefixer.ts | 12 ++++++ src/utils/has-own-property.ts | 7 ++++ test/plugins.ts | 69 +++++++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 src/utils/has-own-property.ts diff --git a/src/plugins.ts b/src/plugins.ts index 0e7d64c..12f0cd6 100644 --- a/src/plugins.ts +++ b/src/plugins.ts @@ -1,4 +1,5 @@ -import { Rule } from './prefixer' +import { Rule, isRule } from './prefixer' +import hasOwnProperty from './utils/has-own-property' export interface RuleSet { selector: string, @@ -9,6 +10,23 @@ type Plugin = (set: RuleSet) => RuleSet const plugins: Plugin[] = [] +function isRuleSet (set: unknown): set is RuleSet { + if (typeof set !== 'object' || set === null) { + return false + } + if (!hasOwnProperty(set, 'selector') || + typeof set.selector !== 'string') { + return false + } + if (!hasOwnProperty(set, 'rules')) { + return false + } + + const rules = set.rules + + return Array.isArray(rules) && rules.every(isRule) +} + /** * Adds a plugin that ui-box will apply before emitting styles into a stylesheet. * Plugins are applied in the *opposite* order of insertion (most recent `use` first). @@ -25,7 +43,14 @@ export function apply(set: RuleSet) { let newSet = set for (const plugin of plugins) { - newSet = plugin({...newSet}) + const updatedSet = plugin({...newSet}) + + // Skip broken plugins + if (isRuleSet(updatedSet)) { + newSet = updatedSet + } else if (process.env.NODE_ENV !== 'production') { + throw new Error(`📦 ui-box: Plugin "${plugin.name}" returned an invalid RuleSet.`) + } } return newSet diff --git a/src/prefixer.ts b/src/prefixer.ts index 999b655..e607b79 100644 --- a/src/prefixer.ts +++ b/src/prefixer.ts @@ -1,5 +1,6 @@ import {prefix} from 'inline-style-prefixer' import decamelize from './utils/decamelize' +import hasOwnProperty from './utils/has-own-property' const prefixRegex = /^(Webkit|ms|Moz|O)/ @@ -7,6 +8,17 @@ export interface Rule { property: string value: string } + +export function isRule(rule: unknown): rule is Rule { + if (typeof rule !== 'object' || rule === null) { + return false + } + return hasOwnProperty(rule, 'property') && + typeof rule.property === 'string' && + hasOwnProperty(rule, 'value') && + typeof rule.value === 'string' +} + /** * Adds vendor prefixes to properties and values. */ diff --git a/src/utils/has-own-property.ts b/src/utils/has-own-property.ts new file mode 100644 index 0000000..6d0e1d8 --- /dev/null +++ b/src/utils/has-own-property.ts @@ -0,0 +1,7 @@ +/** + * Test existence of a property on an object in a Typescript-friendly way + */ +export default function hasOwnProperty + (obj: UnknownObject, prop: Prop): obj is UnknownObject & Record { + return Object.prototype.hasOwnProperty.call(obj, prop) +} diff --git a/test/plugins.ts b/test/plugins.ts index 0142ae5..4d8976e 100644 --- a/test/plugins.ts +++ b/test/plugins.ts @@ -1,7 +1,9 @@ import test from 'ava' import * as plugins from '../src/plugins' +const originalNodeEnv = process.env.NODE_ENV test.afterEach.always(() => { + process.env.NODE_ENV = originalNodeEnv plugins.clear() }) @@ -28,3 +30,70 @@ test('applies a selector prefix to the styles', t => { ] }) }) + +test('removes plugins', t => { + const ruleset = { + selector: '.ub-min-w_10px', + rules: [ + { property: 'min-width', value: '10px' }, + { property: 'user-select', value: 'none' } + ] + } + plugins.use(({ selector, rules }) => { + return { + selector: `#my-div ${selector}`, + rules + } + }) + plugins.clear() + const result = plugins.apply(ruleset) + t.deepEqual(result, ruleset) +}) + +test('errors on plugins in dev that return invalid rules', t => { + const ruleset = { + selector: '.ub-min-w_10px', + rules: [ + { property: 'min-width', value: '10px' }, + { property: 'user-select', value: 'none' } + ] + } + plugins.use(({ selector, rules }) => { + return { + selector: `#my-div ${selector}`, + rules: { bad: 'rule' } + } + }) + t.throws(() => plugins.apply(ruleset), /invalid RuleSet/) +}) + +test('skips plugins in production that return invalid rules', t => { + process.env.NODE_ENV = 'production' + const ruleset = { + selector: '.ub-min-w_10px', + rules: [ + { property: 'min-width', value: '10px' }, + { property: 'user-select', value: 'none' } + ] + } + plugins.use(({ selector, rules }) => { + return { + selector: `#bad-div ${selector}`, + rules: { bad: 'rule' } + } + }) + plugins.use(({ selector, rules }) => { + return { + selector: `#my-div ${selector}`, + rules + } + }) + const result = plugins.apply(ruleset) + t.deepEqual(result, { + selector: '#my-div .ub-min-w_10px', + rules: [ + { property: 'min-width', value: '10px' }, + { property: 'user-select', value: 'none' } + ] + }) +})