From 30b6c4a1551ee7feb66a31c48f38e1841a6ebdb2 Mon Sep 17 00:00:00 2001 From: mauroerta Date: Tue, 18 May 2021 08:29:57 +0200 Subject: [PATCH] feat: jss package and fix to native test --- README.md | 153 ++++++++++++++++++++++++++ apps/benchmarks/package.json | 8 +- apps/benchmarks/results/jss.md | 17 +++ apps/benchmarks/src/jss/index.js | 37 +++++++ apps/benchmarks/src/jss/jssVsCore.js | 29 +++++ apps/benchmarks/src/jss/utils.js | 45 ++++++++ assets/badges/badge-branches.svg | 1 + assets/badges/badge-functions.svg | 1 + assets/badges/badge-lines.svg | 1 + assets/badges/badge-statements.svg | 1 + jest.config.js | 8 +- package.json | 2 +- packages/jss/package.json | 34 ++++++ packages/jss/src/getStyle.ts | 28 +++++ packages/jss/src/index.ts | 1 + packages/jss/src/initJSS.ts | 4 + packages/jss/tests/jss.test.ts | 40 +++++++ packages/jss/tsconfig.json | 8 ++ packages/native/tests/shadows.test.ts | 2 +- 19 files changed, 411 insertions(+), 9 deletions(-) create mode 100644 README.md create mode 100644 apps/benchmarks/results/jss.md create mode 100755 apps/benchmarks/src/jss/index.js create mode 100755 apps/benchmarks/src/jss/jssVsCore.js create mode 100755 apps/benchmarks/src/jss/utils.js create mode 100644 assets/badges/badge-branches.svg create mode 100644 assets/badges/badge-functions.svg create mode 100644 assets/badges/badge-lines.svg create mode 100644 assets/badges/badge-statements.svg create mode 100644 packages/jss/package.json create mode 100644 packages/jss/src/getStyle.ts create mode 100644 packages/jss/src/index.ts create mode 100644 packages/jss/src/initJSS.ts create mode 100644 packages/jss/tests/jss.test.ts create mode 100644 packages/jss/tsconfig.json diff --git a/README.md b/README.md new file mode 100644 index 00000000..e7589984 --- /dev/null +++ b/README.md @@ -0,0 +1,153 @@ +# @morfeo + +Morfeo it's a set of tools that helps you to build **consistent UIs** based on a single source of truth: the **theme**. + +You can use it with any framework like `React`, `React Native`, `Vue`, `Angular`, `svelte` or `vanilla`. + +In other words, morfeo will transform this: + +```typescript +{ + componentName: 'Button', + variant: 'primary', +} +``` + +into this: + +```typescript +{ + color: '#e3e3e3', + backgroundColor: '#fff', + borderRadius: '20px', + '&:hover': { + opacity: 0.4, + }, +} +``` + +Or, into a plain css: + +```css +.button-primary { + color: #e3e3e3; + background-color: #fff; + border-radius: 20px; +} + +.button-primary:hover { + opacity: 0.4; +} +``` + +By using a bit of magic explained [here](#how-it-works) + +morfeo is cross-framework, but to have a faster implementation and a better developer experience we create a set of packages that integrates morfeo with the most used frameworks like: + +**@morfeo/web** required if you're run in a browser + +**@morfeo/native** perfect for React native + +**@morfeo/styled-components-web** deep integration with styled-components + +**@morfeo/angular** **_coming soon_** + +**@morfeo/jss** perfect for svelte + +**@morfeo/hooks** hook for React/React Native + +| Branches | Functions | Lines | Statements | +| ------------------------------------------- | -------------------------------------------- | ---------------------------------------- | --------------------------------------------- | +| ![logo](./assets/badges/badge-branches.svg) | ![logo](./assets/badges/badge-functions.svg) | ![logo](./assets/badges/badge-lines.svg) | ![logo](./assets/badges/badge-statements.svg) | + +## Motivations + +When your application start to grow, maintain UI consistency it's not easy. +Even in popular applications we often face **wrong typographies**, different **color pallettes** used across different pages or inconsistent **spacings** in each component. + +These problems are even more frequent in large applications where different teams works on different features (maybe with different technologies and frameworks). + +**morfeo** solves this problem by sharing across all the application a customizable `theme` that contains the "language" of the application design and a `parser` that generate styles based on this language, in this way the UIs an the components are always consistent. + +## How it works + +The main concepts around morfeo are 2 entities, **theme** and **parsers**: + +`theme` it's an handler that contains the **design language** of your application, for example a set of colors, spacings, shadows, sizes and gradients, and example of theme could be the following: + +```typescript +import { theme } from '@morfeo/core'; + +const defaultTheme = { + color: { + primary: '#000', + secondary: '#e3e3e3', + danger: '#eb2f06', + success: '#78e08f', + warning: '#fa983a', + }, + space: { + xxs: '8px', + xs: '16px', + s: '24px', + m: '32px', + l: '40px', + xl: '48px', + xxl: '56px', + }, + components: { + Button: { + style: { + color: 'primary', + padding: 's', + }, + }, + }, +}; + +// From now on the theme will be available in all your application +theme.set(defaultTheme); + +console.log(theme.get()); // will log an object equals to `defaultTheme` +console.log(theme.getValue('colors', 'primary')); // will log #000 +``` + +Once you have a centralized theme, you need to parse this theme to generate the style for your components an layouts, here is where the `parsers` handler start to play! + +This is an example with React + +```tsx +import { theme, parsers } from '@morfeo/core'; + +function Button() { + const style = parsers.resolve({ style: { componentName: 'Button' } }); + + return ; +} +``` + +Or if you want, you can use the hooks inside the package `@morfeo/hook` + +```tsx +import { theme } from '@morfeo/core'; +import { useStyle } from '@morfeo/hooks'; + +function Button() { + const style = useStyle({ componentName: 'Button' }); + + return ; +} +``` + +The value of `style` will be in this example equals to: + +```json +{ + "color": "#000", + "padding": "16px" +} +``` + +## Features + +WIP diff --git a/apps/benchmarks/package.json b/apps/benchmarks/package.json index 03447c1f..1aab0f82 100755 --- a/apps/benchmarks/package.json +++ b/apps/benchmarks/package.json @@ -1,6 +1,6 @@ { "name": "benchmarks", - "version": "0.0.7", + "version": "0.0.1", "private": true, "author": { "name": "Mauro Erta", @@ -9,13 +9,15 @@ "scripts": { "morfeo-vs": "node src/morfeo-vs/index.js", "core": "node src/core/index.js", - "all": "npm run core && npm run morfeo-vs" + "jss": "node src/jss/index.js", + "all": "npm run core && npm run jss && npm run morfeo-vs" }, "devDependencies": { "benchmark": "^2.1.4" }, "dependencies": { - "@morfeo/core": "^0.0.7", + "@morfeo/core": "^0.0.8", + "@morfeo/jss": "^0.0.8", "styled-system": "5.1.5" } } diff --git a/apps/benchmarks/results/jss.md b/apps/benchmarks/results/jss.md new file mode 100644 index 00000000..8b265846 --- /dev/null +++ b/apps/benchmarks/results/jss.md @@ -0,0 +1,17 @@ +# @morfeo/jss + +## @morfeo/jss speed compared to @morfeo/core + +```json +{ + "button": { + "color": "primary" + } +} +``` + +**regular parsing** 2,588,520 ops/sec ±1.37% (89 runs sampled) + +**with cache enabled** 213,422 ops/sec ±2.30% (85 runs sampled) + +Fastest is **regular parsing** diff --git a/apps/benchmarks/src/jss/index.js b/apps/benchmarks/src/jss/index.js new file mode 100755 index 00000000..9ecfe79e --- /dev/null +++ b/apps/benchmarks/src/jss/index.js @@ -0,0 +1,37 @@ +const { theme } = require('@morfeo/core'); +const { writeMdTitle } = require('./utils'); +const jssVsCoreSuite = require('./jssVsCore'); + +theme.set({ + colors: { + primary: 'black', + secondary: 'white', + }, + space: { + s: '10px', + m: '20px', + }, + shadows: { + light: { + color: 'primary', + offset: { width: 2, height: 2 }, + opacity: 0.4, + radius: 20, + }, + }, + components: { + Box: { + style: { + bg: 'primary', + color: 'secondary', + px: 'm', + my: 's', + shadow: 'light', + }, + }, + }, +}); + +writeMdTitle(`# @morfeo/jss`); + +jssVsCoreSuite.run({ async: false }); diff --git a/apps/benchmarks/src/jss/jssVsCore.js b/apps/benchmarks/src/jss/jssVsCore.js new file mode 100755 index 00000000..27cfcaf1 --- /dev/null +++ b/apps/benchmarks/src/jss/jssVsCore.js @@ -0,0 +1,29 @@ +const Benchmark = require('benchmark'); +const { parsers } = require('@morfeo/core'); +const { getStyle } = require('@morfeo/jss'); +const { onCycle, onComplete, onStart } = require('./utils'); + +const suite = new Benchmark.Suite(); + +const styles = { button: { color: 'primary' } }; + +suite + .add('regular parsing', () => { + Object.keys(styles).reduce((acc, key) => { + const style = styles[key]; + return { + ...acc, + [key]: parsers.resolve({ style }), + }; + }, {}); + }) + .add('with cache enabled', () => { + getStyle(styles); + }) + .on('start', () => + onStart('@morfeo/jss speed compared to @morfeo/core', styles), + ) + .on('cycle', onCycle) + .on('complete', () => onComplete(suite)); + +module.exports = suite; diff --git a/apps/benchmarks/src/jss/utils.js b/apps/benchmarks/src/jss/utils.js new file mode 100755 index 00000000..8e28d2ef --- /dev/null +++ b/apps/benchmarks/src/jss/utils.js @@ -0,0 +1,45 @@ +const { + getMdPath, + onCycle: _onCycle, + onStart: _onStart, + writeInMd: _writeInMd, + appendInMd: _appendInMd, + onComplete: _onComplete, + writeMdTitle: _writeMdTitle, +} = require('../utils'); + +const mdPath = getMdPath('jss'); + +function writeInMd(text) { + _writeInMd(mdPath, text); +} + +function appendInMd(text) { + appendInMd(mdPath, `\n\n${text}`); +} + +function writeMdTitle(title) { + _writeInMd(mdPath, `${title}`); +} + +function onStart(title, style) { + _onStart(mdPath, title, style); +} + +function onCycle(event) { + _onCycle(mdPath, event); +} + +function onComplete(suite) { + _onComplete(mdPath, suite); +} + +module.exports = { + onCycle, + onStart, + getMdPath, + writeInMd, + appendInMd, + onComplete, + writeMdTitle, +}; diff --git a/assets/badges/badge-branches.svg b/assets/badges/badge-branches.svg new file mode 100644 index 00000000..e9dbd7c6 --- /dev/null +++ b/assets/badges/badge-branches.svg @@ -0,0 +1 @@ +Coverage:branches: 100%Coverage:branches100% \ No newline at end of file diff --git a/assets/badges/badge-functions.svg b/assets/badges/badge-functions.svg new file mode 100644 index 00000000..330a44e2 --- /dev/null +++ b/assets/badges/badge-functions.svg @@ -0,0 +1 @@ +Coverage:functions: 100%Coverage:functions100% \ No newline at end of file diff --git a/assets/badges/badge-lines.svg b/assets/badges/badge-lines.svg new file mode 100644 index 00000000..cbb01ce9 --- /dev/null +++ b/assets/badges/badge-lines.svg @@ -0,0 +1 @@ +Coverage:lines: 100%Coverage:lines100% \ No newline at end of file diff --git a/assets/badges/badge-statements.svg b/assets/badges/badge-statements.svg new file mode 100644 index 00000000..3f8ebb1f --- /dev/null +++ b/assets/badges/badge-statements.svg @@ -0,0 +1 @@ +Coverage:statements: 100%Coverage:statements100% \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index e4128323..220f744d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -44,10 +44,10 @@ module.exports = { }, coverageThreshold: { global: { - branches: 80, - functions: 80, - lines: 80, - statements: 80, + branches: 90, + functions: 90, + lines: 90, + statements: 90, }, }, }; diff --git a/package.json b/package.json index 508cc6f0..962464ce 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "test": "jest --updateSnapshot", "test:watch": "jest --watch --updateSnapshot", "test:coverage": "jest --coverage --updateSnapshot", - "test:badges": "npm run test:coverage && jest-coverage-badges", + "test:badges": "npm run test:coverage && jest-coverage-badges output \"./assets/badges\"", "publish": "npx lerna publish from-package --yes", "version": "npx lerna version patch --no-push --no-git-tag-version --conventional-commits" }, diff --git a/packages/jss/package.json b/packages/jss/package.json new file mode 100644 index 00000000..de751533 --- /dev/null +++ b/packages/jss/package.json @@ -0,0 +1,34 @@ +{ + "name": "@morfeo/jss", + "author": { + "name": "Mauro Erta", + "email": "mauro@vlkstudio.com" + }, + "private": false, + "version": "0.0.8", + "license": "MIT", + "main": "build/index.js", + "module": "build/index.js", + "types": "build/index", + "typings": "build/index", + "keywords": [ + "design", + "system", + "jss" + ], + "scripts": { + "build": "rimraf build && tsc", + "watch": "tsc -w" + }, + "dependencies": { + "@morfeo/web": "^0.0.8", + "jss": "^10.6.0", + "jss-preset-default": "^10.6.0" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "build" + ] +} diff --git a/packages/jss/src/getStyle.ts b/packages/jss/src/getStyle.ts new file mode 100644 index 00000000..1ea1a250 --- /dev/null +++ b/packages/jss/src/getStyle.ts @@ -0,0 +1,28 @@ +import { parsers, Style, ResolvedStyle } from '@morfeo/web'; +import jss, { StyleSheetFactoryOptions } from 'jss'; +import './initJSS'; + +export function getStyleSheet( + styles: Record, + options?: StyleSheetFactoryOptions, +) { + const parsedStyle = Object.keys(styles).reduce((acc, key) => { + const style = styles[key]; + return { + ...acc, + [key]: parsers.resolve({ style }), + }; + }, {} as Record); + + return jss.createStyleSheet(parsedStyle as any, options); +} + +export function getStyle( + styles: Record, + options?: StyleSheetFactoryOptions, +) { + const sheet = getStyleSheet(styles, options); + sheet.attach(); + + return sheet.classes; +} diff --git a/packages/jss/src/index.ts b/packages/jss/src/index.ts new file mode 100644 index 00000000..d1f30664 --- /dev/null +++ b/packages/jss/src/index.ts @@ -0,0 +1 @@ +export * from './getStyle'; diff --git a/packages/jss/src/initJSS.ts b/packages/jss/src/initJSS.ts new file mode 100644 index 00000000..1bc9017e --- /dev/null +++ b/packages/jss/src/initJSS.ts @@ -0,0 +1,4 @@ +import jss from 'jss'; +import preset from 'jss-preset-default'; + +jss.setup(preset()); diff --git a/packages/jss/tests/jss.test.ts b/packages/jss/tests/jss.test.ts new file mode 100644 index 00000000..2f9e4fdf --- /dev/null +++ b/packages/jss/tests/jss.test.ts @@ -0,0 +1,40 @@ +import { Theme, theme } from '@morfeo/web'; +import { getStyle, getStyleSheet } from '../src'; + +const THEME: Theme = { + colors: { + primary: '#e3e3e3', + secondary: '#000', + }, +} as any; + +describe('jss', () => { + beforeAll(() => { + theme.set(THEME); + }); + afterAll(() => { + theme.reset(); + }); + + test('should generate the sheet', () => { + const sheet = getStyleSheet({ + button: { + bg: 'primary', + }, + }); + + expect(sheet.getRule('button').toString()).toContain( + 'background-color: #e3e3e3;', + ); + }); + + test('should generate the classnames', () => { + const classes = getStyle({ + button: { + bg: 'primary', + }, + }); + + expect(classes).toHaveProperty('button'); + }); +}); diff --git a/packages/jss/tsconfig.json b/packages/jss/tsconfig.json new file mode 100644 index 00000000..9ee04b7b --- /dev/null +++ b/packages/jss/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./build", + }, + "include": ["./src"], + "exclude": ["./node_modules", "./src/**/*.test.ts", "./src/**/*.spec.ts"] +} diff --git a/packages/native/tests/shadows.test.ts b/packages/native/tests/shadows.test.ts index 5ccb4fe1..e47b998c 100644 --- a/packages/native/tests/shadows.test.ts +++ b/packages/native/tests/shadows.test.ts @@ -70,7 +70,7 @@ describe('shadows', () => { test('should generate the property `shadow` based on the medium shadow', () => { const result = parsers.resolve({ style: { shadow: 'medium' } }); - console.log(parsers.cache); + expect(result).toEqual({ elevation: 2, shadowColor: 'white',