├── packages ├── core │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ ├── test │ │ ├── compose.test.tsx │ │ └── withBemMod.test.tsx │ └── README.md ├── di │ ├── .gitignore │ ├── package-lock.json │ ├── tsconfig.json │ ├── package.json │ ├── di.tsx │ ├── README.md │ ├── CHANGELOG.md │ └── test │ │ └── di.test.tsx ├── classname │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ ├── README.md │ ├── classname.ts │ ├── test │ │ └── classname.test.ts │ └── CHANGELOG.md ├── classnames │ ├── .gitignore │ ├── tsconfig.json │ ├── test │ │ └── classnames.test.ts │ ├── README.md │ ├── classnames.ts │ ├── package.json │ └── CHANGELOG.md ├── pack │ ├── .gitignore │ ├── .eslintrc │ ├── src │ │ ├── debug.ts │ │ ├── stdout.ts │ │ ├── measurePerf.ts │ │ ├── wrapToPromise.ts │ │ ├── loadConfig.ts │ │ ├── plugins │ │ │ ├── CleanUpPlugin.ts │ │ │ ├── PackageJsonPlugin.ts │ │ │ ├── TypeScriptResolvePlugin.ts │ │ │ ├── CopyAssetsPlugin.ts │ │ │ ├── CssPlugin.ts │ │ │ ├── TypeScriptPlugin.ts │ │ │ └── BabelTypeScriptPlugin.ts │ │ ├── cli │ │ │ └── build.ts │ │ ├── interfaces.ts │ │ ├── progress.ts │ │ └── build.ts │ ├── bin │ │ └── pack │ ├── tsconfig.json │ ├── package.json │ ├── CHANGELOG.md │ └── README.md ├── eslint-plugin │ ├── index.js │ ├── package.json │ ├── README.md │ ├── docs │ │ └── rules │ │ │ ├── no-classname-runtime.md │ │ │ └── whitelist-levels-imports.md │ ├── CHANGELOG.md │ ├── tests │ │ └── lib │ │ │ └── rules │ │ │ ├── no-classname-runtime.js │ │ │ └── whitelist-levels-imports.js │ └── lib │ │ └── rules │ │ ├── whitelist-levels-imports.js │ │ └── no-classname-runtime.js └── webpack-exp-plugin │ ├── CHANGELOG.md │ ├── index.js │ ├── replacer.js │ ├── package.json │ ├── Readme.md │ └── replacer.test.js ├── .npmrc ├── website ├── .npmrc ├── docs │ ├── guides │ │ ├── naming.md │ │ ├── start.md │ │ ├── experiments.md │ │ ├── structure.md │ │ └── lazy.md │ ├── api │ │ ├── core │ │ │ ├── compose.md │ │ │ └── core.md │ │ ├── di │ │ │ └── api.md │ │ ├── classnames │ │ │ ├── api.md │ │ │ └── classnames.md │ │ └── classname │ │ │ ├── ClassNameInitilizer.md │ │ │ ├── api.md │ │ │ ├── Preset.md │ │ │ ├── ClassNameFormatter.md │ │ │ ├── withNaming.md │ │ │ └── cn.md │ └── introduction │ │ └── quick-start.md ├── static │ └── img │ │ ├── favicon.ico │ │ └── logo.svg ├── .gitignore ├── src │ ├── pages │ │ ├── styles.module.css │ │ └── index.js │ └── css │ │ └── custom.css ├── package.json ├── sidebars.js ├── README.md └── docusaurus.config.js ├── .eslintignore ├── .commitlintrc.json ├── environment.d.ts ├── .huskyrc ├── .config └── jest │ ├── jest.setup.js │ └── jest.config.js ├── .gitignore ├── .prettierrc ├── .editorconfig ├── .lintstagedrc ├── lerna.json ├── tsconfig.json ├── .github └── workflows │ └── ci.yml ├── package.json ├── CONTRIBUTING.md ├── README.md ├── scripts └── rollup │ └── build.js └── .eslintrc /packages/core/.gitignore: -------------------------------------------------------------------------------- 1 | core.d.ts 2 | -------------------------------------------------------------------------------- /packages/di/.gitignore: -------------------------------------------------------------------------------- 1 | di.d.ts 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save=false 3 | -------------------------------------------------------------------------------- /packages/classname/.gitignore: -------------------------------------------------------------------------------- 1 | classname.d.ts 2 | -------------------------------------------------------------------------------- /packages/classnames/.gitignore: -------------------------------------------------------------------------------- 1 | classnames.d.ts 2 | -------------------------------------------------------------------------------- /website/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save=false 3 | -------------------------------------------------------------------------------- /packages/pack/.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | oclif.manifest.json 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | !.eslintrc.js 2 | 3 | build 4 | lib 5 | node_modules 6 | 7 | *.d.ts 8 | -------------------------------------------------------------------------------- /website/docs/guides/naming.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: naming 3 | title: Naming 4 | --- 5 | 6 | soon. 7 | -------------------------------------------------------------------------------- /website/docs/guides/start.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: start 3 | title: Start 4 | --- 5 | 6 | soon. 7 | -------------------------------------------------------------------------------- /website/docs/api/core/compose.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: compose 3 | title: compose 4 | --- 5 | 6 | soon. 7 | -------------------------------------------------------------------------------- /website/docs/api/core/core.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: core 3 | title: API Reference 4 | --- 5 | 6 | soon. 7 | -------------------------------------------------------------------------------- /website/docs/api/di/api.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: di-api 3 | title: API Reference 4 | --- 5 | 6 | soon. 7 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@commitlint/config-conventional" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /website/docs/guides/experiments.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: experiments 3 | title: Experiments 4 | --- 5 | 6 | soon. 7 | -------------------------------------------------------------------------------- /website/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bem/bem-react/HEAD/website/static/img/favicon.ico -------------------------------------------------------------------------------- /packages/pack/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": 0, 4 | "no-use-before-define": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/pack/src/debug.ts: -------------------------------------------------------------------------------- 1 | import createDebug from 'debug' 2 | 3 | export const mark = createDebug('@bem-react/pack') 4 | -------------------------------------------------------------------------------- /environment.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Eenvironment flag for development. 3 | * @internal 4 | */ 5 | declare const __DEV__: boolean 6 | -------------------------------------------------------------------------------- /packages/di/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bem-react/di", 3 | "version": "5.1.0", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged", 4 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.config/jest/jest.setup.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | global.requestAnimationFrame = (callback) => { 4 | setTimeout(callback, 0) 5 | } 6 | 7 | global.__DEV__ = true 8 | -------------------------------------------------------------------------------- /website/docs/api/classnames/api.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: classnames-api 3 | title: API Reference 4 | --- 5 | 6 | #### Top-Level exports 7 | 8 | - [classnames](classnames) 9 | -------------------------------------------------------------------------------- /packages/pack/bin/pack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('@oclif/core') 4 | .run() 5 | .then(require('@oclif/core/flush')) 6 | .catch(require('@oclif/core/handle')) 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | coverage 3 | /lib 4 | umd 5 | node_modules 6 | *.log 7 | reference 8 | 9 | # Rollup 10 | 11 | .rpt2_cache 12 | 13 | # Build 14 | 15 | build 16 | -------------------------------------------------------------------------------- /packages/pack/src/stdout.ts: -------------------------------------------------------------------------------- 1 | export const stdout = { 2 | plain(m: string): void { 3 | process.stdout.write(m + '\n') 4 | }, 5 | error(m: string): void { 6 | process.stdout.write(m + '\n') 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "endOfLine": "lf", 4 | "parser": "typescript", 5 | "printWidth": 100, 6 | "semi": false, 7 | "singleQuote": true, 8 | "tabWidth": 2, 9 | "trailingComma": "all" 10 | } 11 | -------------------------------------------------------------------------------- /packages/eslint-plugin/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'no-classname-runtime': require('./lib/rules/no-classname-runtime'), 4 | 'whitelist-levels-imports': require('./lib/rules/whitelist-levels-imports'), 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /website/docs/introduction/quick-start.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: installation 3 | title: Quick Start 4 | --- 5 | 6 | For start usage need install all necessary packages: 7 | 8 | ```sh 9 | npm i -P @bem-react/core @bem-react/di @bem-react/classname 10 | ``` 11 | -------------------------------------------------------------------------------- /packages/di/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | /* Basic Options */ 5 | "rootDir": ".", 6 | "declarationDir": ".", 7 | "outDir": "build" 8 | }, 9 | "include": ["../../environment.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | /* Basic Options */ 5 | "rootDir": ".", 6 | "declarationDir": ".", 7 | "outDir": "build" 8 | }, 9 | "include": ["../../environment.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/classname/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | /* Basic Options */ 5 | "rootDir": ".", 6 | "declarationDir": ".", 7 | "outDir": "build" 8 | }, 9 | "include": ["../../environment.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/classnames/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | /* Basic Options */ 5 | "rootDir": ".", 6 | "declarationDir": ".", 7 | "outDir": "build" 8 | }, 9 | "include": ["../../environment.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | end_of_line = lf 10 | insert_final_newline = true 11 | max_line_length = 120 12 | -------------------------------------------------------------------------------- /website/docs/api/classname/ClassNameInitilizer.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: ClassNameInitilizer 3 | title: ClassNameInitilizer 4 | hide_title: true 5 | --- 6 | 7 | # `ClassNameInitilizer` 8 | 9 | ```ts 10 | type ClassNameInitilizer = (blockName: string, elemName?: string) => ClassNameFormatter 11 | ``` 12 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,ts,tsx}": [ 3 | "prettier --write", 4 | "eslint --fix", 5 | "git add" 6 | ], 7 | "*.md": [ 8 | "prettier --write --parser markdown", 9 | "git add" 10 | ], 11 | "*.json": [ 12 | "prettier --write --parser json", 13 | "git add" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/pack/src/measurePerf.ts: -------------------------------------------------------------------------------- 1 | import { performance } from 'perf_hooks' 2 | 3 | export function measurePerf(): () => string { 4 | const start = performance.now() 5 | return () => { 6 | const end = performance.now() 7 | const time = `${((end - start) / 1000).toFixed(2)}s` 8 | return time 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /website/docs/api/classname/api.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: classname-api 3 | title: API Reference 4 | --- 5 | 6 | #### Top-Level exports 7 | 8 | - [cn](cn) 9 | - [withNaming](withNaming) 10 | 11 | #### Types 12 | 13 | - [Preset](Preset) 14 | - [ClassNameInitilizer](ClassNameInitilizer) 15 | - [ClassNameFormatter](ClassNameFormatter) 16 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /packages/pack/src/wrapToPromise.ts: -------------------------------------------------------------------------------- 1 | // TODO: Remove this logic. 2 | export function wrapToPromise( 3 | fn: (resolve: any, payload: any) => void, 4 | payload: any, 5 | ): Promise { 6 | return new Promise(async (resolve, reject) => { 7 | try { 8 | await fn(resolve, payload) 9 | } catch (error) { 10 | reject(error) 11 | } 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /website/src/pages/styles.module.css: -------------------------------------------------------------------------------- 1 | .heroBanner { 2 | padding: 4rem 0; 3 | text-align: center; 4 | position: relative; 5 | overflow: hidden; 6 | } 7 | 8 | @media screen and (max-width: 966px) { 9 | .heroBanner { 10 | padding: 2rem; 11 | } 12 | } 13 | 14 | .buttons { 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | } 19 | -------------------------------------------------------------------------------- /packages/webpack-exp-plugin/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # 0.1.0 (2020-12-23) 7 | 8 | ### Features 9 | 10 | - wepack exp plugin ([4bb6ab0](https://github.com/bem/bem-react/commit/4bb6ab0be3f0ca3717eec13ccb828365cd340bf8)) 11 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "command": { 4 | "publish": { 5 | "allowBranch": "master", 6 | "ignoreChanges": ["*.md", "**/test/**"], 7 | "message": "chore(release): publish" 8 | } 9 | }, 10 | "independent": true, 11 | "conventionalCommits": true, 12 | "version": "independent", 13 | "exact": true, 14 | "npmClientArgs": ["--no-package-lock"] 15 | } 16 | -------------------------------------------------------------------------------- /website/docs/api/classname/Preset.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: Preset 3 | title: Preset 4 | hide_title: true 5 | --- 6 | 7 | # `Preset` 8 | 9 | ```ts 10 | type Preset = { 11 | /** 12 | * Global namespace. 13 | */ 14 | n?: string 15 | /** 16 | * Element delimiter. 17 | */ 18 | e?: string 19 | /** 20 | * Modifier delimiter. 21 | */ 22 | m?: string 23 | /** 24 | * Modifier value delimiter. 25 | */ 26 | v?: string 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /.config/jest/jest.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { resolve } = require('path') 4 | 5 | /** 6 | * @type {import('@jest/types').Config.InitialOptions} 7 | */ 8 | module.exports = { 9 | rootDir: process.cwd(), 10 | setupFiles: [resolve(__dirname, 'jest.setup.js')], 11 | preset: 'ts-jest', 12 | testMatch: ['/**/*.test.{ts,tsx,js}'], 13 | collectCoverageFrom: ['**/*.{ts,tsx}', '!**/*.d.ts', '!**/*.test.{ts,tsx}'], 14 | testEnvironment: 'jsdom', 15 | } 16 | -------------------------------------------------------------------------------- /website/docs/api/classname/ClassNameFormatter.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: ClassNameFormatter 3 | title: ClassNameFormatter 4 | hide_title: true 5 | --- 6 | 7 | # `ClassNameFormatter` 8 | 9 | ```ts 10 | type ClassNameList = Array 11 | type NoStrictEntityMods = Record 12 | 13 | type ClassNameFormatter = ( 14 | elementNameOrBlockModifiers?: NoStrictEntityMods | string | null, 15 | elementModifiersOrBlockMixins?: NoStrictEntityMods | ClassNameList | null, 16 | elementMixins?: ClassNameList, 17 | ) => string 18 | ``` 19 | -------------------------------------------------------------------------------- /packages/classnames/test/classnames.test.ts: -------------------------------------------------------------------------------- 1 | import { classnames } from '../classnames' 2 | 3 | describe('@bem-react/classnames', () => { 4 | describe('classnames', () => { 5 | test('empty', () => { 6 | expect(classnames()).toEqual('') 7 | }) 8 | 9 | test('undefined', () => { 10 | expect(classnames('Block', undefined, 'Block2')).toEqual('Block Block2') 11 | }) 12 | 13 | test('uniq', () => { 14 | expect(classnames('CompositeBlock', 'Block', 'Test', 'Block')).toEqual( 15 | 'CompositeBlock Block Test', 16 | ) 17 | }) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /website/docs/api/classnames/classnames.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: classnames 3 | title: classnames 4 | hide_title: true 5 | --- 6 | 7 | # `classnames` 8 | 9 | Generate className string with unique tokens. 10 | 11 | #### Arguments 12 | 13 | 1. `...tokens` (Array): ClassName tokens. 14 | 15 | #### Returns 16 | 17 | `(string)` 18 | 19 | #### Examples 20 | 21 | Remove all duplicates or undefined values: 22 | 23 | ```ts 24 | import { classnames } from '@bem-react/classnames' 25 | 26 | classnames('Button', 'Button', 'Header-Button', undefined) // -> Button Header-Button 27 | ``` 28 | -------------------------------------------------------------------------------- /packages/webpack-exp-plugin/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { NormalModuleReplacementPlugin } = require('webpack') 4 | const replacerToExp = require('./replacer') 5 | 6 | class WebpackExpPlugin extends NormalModuleReplacementPlugin { 7 | constructor(packageName, experiments = {}) { 8 | if (!packageName) { 9 | throw new Error('Missing package name.') 10 | } 11 | 12 | const replacer = replacerToExp.bind(null, packageName, experiments) 13 | 14 | super(new RegExp(packageName), (result) => { 15 | result.request = replacer(result.request) 16 | }) 17 | } 18 | } 19 | 20 | module.exports = WebpackExpPlugin 21 | -------------------------------------------------------------------------------- /packages/classnames/README.md: -------------------------------------------------------------------------------- 1 | # @bem-react/classnames · [![npm (scoped)](https://img.shields.io/npm/v/@bem-react/classname.svg)](https://www.npmjs.com/package/@bem-react/classnames) [![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/@bem-react/classnames.svg)](https://bundlephobia.com/result?p=@bem-react/classnames) 2 | 3 | Tiny helper for merging CSS classes. 4 | 5 | ## Install 6 | 7 | ``` 8 | npm i -S @bem-react/classnames 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```ts 14 | import { classnames } from '@bem-react/classnames' 15 | 16 | classnames('Block', undefined, 'Block2', 'Block') // Block Block2 17 | ``` 18 | -------------------------------------------------------------------------------- /packages/pack/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "rootDir": "./src", 5 | "outDir": "./lib", 6 | "moduleResolution": "node", 7 | "target": "ES2017", 8 | "module": "CommonJS", 9 | "declaration": true, 10 | /* Module Resolution Options */ 11 | "esModuleInterop": true, 12 | /* Strict Type-Checking Options */ 13 | "strict": true, 14 | /* Additional Checks */ 15 | "forceConsistentCasingInFileNames": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "noImplicitReturns": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /website/src/css/custom.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --ifm-color-primary: #7d53ed; 3 | --ifm-color-primary-dark: #6736ea; 4 | --ifm-color-primary-darker: #5c28e8; 5 | --ifm-color-primary-darkest: #4715cb; 6 | --ifm-color-primary-light: #9370f0; 7 | --ifm-color-primary-lighter: #9e7ef2; 8 | --ifm-color-primary-lightest: #bfaaf6; 9 | --ifm-code-font-size: 95%; 10 | } 11 | 12 | .docusaurus-highlight-code-line { 13 | background-color: rgb(72, 77, 91); 14 | display: block; 15 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 16 | padding: 0 var(--ifm-pre-padding); 17 | } 18 | 19 | .navbar .navbar__logo { 20 | width: 44px; 21 | height: 44px; 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "declaration": true, 5 | "jsx": "react", 6 | "lib": ["es6", "dom"], 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "target": "es5", 10 | /* Module Resolution Options */ 11 | "esModuleInterop": true, 12 | /* Strict Type-Checking Options */ 13 | "strict": true, 14 | /* Additional Checks */ 15 | "forceConsistentCasingInFileNames": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "noImplicitReturns": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true 20 | }, 21 | "exclude": ["**/test/**"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/pack/src/loadConfig.ts: -------------------------------------------------------------------------------- 1 | import { Config } from './interfaces' 2 | import { stdout } from './stdout' 3 | 4 | function normalizeConfig(config: Config): Config[] { 5 | return Array.isArray(config) ? config : [config] 6 | } 7 | 8 | export function loadConfig(p: string): Config[] { 9 | try { 10 | const configs = normalizeConfig(require(p)) 11 | 12 | return configs 13 | } catch (e) { 14 | if ((e as { code: string }).code !== 'MODULE_NOT_FOUND') { 15 | throw e 16 | } 17 | stdout.error(`Cannot load config by "${p}" path, please check path for correct.`) 18 | // TODO: Remove this, and make solution without throw. 19 | throw e 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "docusaurus start", 7 | "build": "docusaurus build", 8 | "swizzle": "docusaurus swizzle", 9 | "deploy": "docusaurus deploy" 10 | }, 11 | "dependencies": { 12 | "@docusaurus/core": "^2.0.0-alpha.40", 13 | "@docusaurus/preset-classic": "^2.0.0-alpha.40", 14 | "classnames": "^2.2.6", 15 | "react": "^16.8.4", 16 | "react-dom": "^16.8.4" 17 | }, 18 | "browserslist": { 19 | "production": [">0.2%", "not dead", "not op_mini all"], 20 | "development": ["last 1 chrome version", "last 1 firefox version", "last 1 safari version"] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /website/sidebars.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | docs: { 3 | Introduction: ['introduction/installation'], 4 | Guides: ['guides/naming', 'guides/structure', 'guides/lazy', 'guides/experiments'], 5 | }, 6 | api: { 7 | '@bem-react/core': ['api/core/core', 'api/core/compose'], 8 | '@bem-react/di': ['api/di/di-api'], 9 | '@bem-react/classname': [ 10 | 'api/classname/classname-api', 11 | 'api/classname/cn', 12 | 'api/classname/withNaming', 13 | 'api/classname/Preset', 14 | 'api/classname/ClassNameInitilizer', 15 | 'api/classname/ClassNameFormatter', 16 | ], 17 | '@bem-react/classnames': ['api/classnames/classnames-api', 'api/classnames/classnames'], 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /packages/eslint-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bem-react/eslint-plugin", 3 | "version": "1.1.2", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "description": "ESLint plugin for projects based on BEM React", 8 | "homepage": "https://github.com/bem/bem-react/tree/master/packages/eslint-plugin", 9 | "repository": "https://github.com/bem/bem-react", 10 | "keywords": [ 11 | "eslint", 12 | "eslintplugin", 13 | "eslint-plugin", 14 | "bem", 15 | "bem-react", 16 | "redefinition-levels" 17 | ], 18 | "main": "index.js", 19 | "license": "MPL-2.0", 20 | "scripts": { 21 | "test": "mocha tests --recursive" 22 | }, 23 | "devDependencies": { 24 | "eslint": "6.3.0", 25 | "mocha": "3.1.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/classnames/classnames.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate className string with unique tokens. 3 | * 4 | * @example 5 | * classnames('Button', 'Header-Button', undefined) // -> Button Header-Button 6 | * 7 | * @param tokens ClassNames tokens. 8 | */ 9 | export function classnames(...tokens: Array): string { 10 | let className = '' 11 | const uniqueCache = new Set() 12 | const classNameList = tokens.join(' ').split(' ') 13 | 14 | for (const value of classNameList) { 15 | if (value === '' || uniqueCache.has(value)) { 16 | continue 17 | } 18 | 19 | uniqueCache.add(value) 20 | 21 | if (className.length > 0) { 22 | className += ' ' 23 | } 24 | 25 | className += value 26 | } 27 | 28 | return className 29 | } 30 | -------------------------------------------------------------------------------- /packages/webpack-exp-plugin/replacer.js: -------------------------------------------------------------------------------- 1 | const replacer = (packageName, experiments, path) => { 2 | // если импорт не целевая бибилотека или содержит эксперимент, то пропускам 3 | if (path.indexOf(packageName) !== 0 || path.indexOf(`${packageName}/experiments`) === 0) { 4 | return path 5 | } 6 | 7 | const blocks = Object.keys(experiments) 8 | 9 | for (let block of blocks) { 10 | const blockPath = block === '*' ? '' : `/${block}/` 11 | const expName = experiments[block] 12 | const pathMatcher = `${packageName}${blockPath}` 13 | 14 | if (!path.startsWith(pathMatcher)) continue 15 | 16 | return path.replace(pathMatcher, `${packageName}/experiments/${expName}${blockPath}`) 17 | } 18 | 19 | return path 20 | } 21 | 22 | module.exports = replacer 23 | -------------------------------------------------------------------------------- /packages/webpack-exp-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bem-react/webpack-exp-plugin", 3 | "version": "0.1.0", 4 | "description": "webpack plugin – replace imports for experiments", 5 | "homepage": "https://github.com/bem/bem-react/tree/master/packages/webpack-exp-plugin", 6 | "repository": "https://github.com/bem/bem-react", 7 | "publishConfig": { 8 | "access": "public" 9 | }, 10 | "main": "index.js", 11 | "scripts": { 12 | "unit": "../../node_modules/.bin/jest --config ../../.config/jest/jest.config.js" 13 | }, 14 | "files": ["replacer.js"], 15 | "keywords": ["bem", "experiments"], 16 | "license": "MPL-2.0", 17 | "devDependencies": { 18 | "jest": "^26.6.3", 19 | "webpack": "^5.4.0" 20 | }, 21 | "peerDependencies": { 22 | "webpack": "^4.0.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/pack/src/plugins/CleanUpPlugin.ts: -------------------------------------------------------------------------------- 1 | import { remove } from 'fs-extra' 2 | import { OnDone, Plugin } from '../interfaces' 3 | import { mark } from '../debug' 4 | 5 | /** 6 | * A list of directories which need to be cleaned. 7 | */ 8 | type Sources = string[] 9 | 10 | class CleanUpPlugin implements Plugin { 11 | name = 'CleanUpPlugin' 12 | 13 | constructor(public sources: Sources) { 14 | mark('CleanUpPlugin::constructor') 15 | } 16 | 17 | async onBeforeRun(done: OnDone) { 18 | mark('CleanUpPlugin::onBeforeRun(start)') 19 | for (const source of this.sources) { 20 | await remove(source) 21 | } 22 | mark('CleanUpPlugin::onBeforeRun(finish)') 23 | done() 24 | } 25 | } 26 | 27 | export function useCleanUpPlugin(sources: Sources): CleanUpPlugin { 28 | return new CleanUpPlugin(sources) 29 | } 30 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ npm 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ npm start 15 | ``` 16 | 17 | This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ npm build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | ``` 30 | $ GIT_USER= USE_SSH=true npm deploy 31 | ``` 32 | 33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 34 | -------------------------------------------------------------------------------- /website/docs/api/classname/withNaming.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: withNaming 3 | title: withNaming 4 | hide_title: true 5 | --- 6 | 7 | # `withNaming` 8 | 9 | Creates a new `ClassNameInitilizer` with passed [preset](Preset). 10 | 11 | #### Arguments 12 | 13 | 1. `preset` (Preset): Shape with naming preset. 14 | 15 | #### Returns 16 | 17 | `ClassNameInitilizer` 18 | 19 | #### Examples 20 | 21 | React naming preset: 22 | 23 | ```ts 24 | import { withNaming } from '@bem-react/classname' 25 | 26 | const preset = { e: '-', m: '_' } 27 | const cn = withNaming(preset) 28 | 29 | cn('Block', 'Elem')({ theme: 'default' }) // -> Block-Elem_theme_default 30 | ``` 31 | 32 | Origin naming preset: 33 | 34 | ```ts 35 | import { withNaming } from '@bem-react/classname' 36 | 37 | const preset = { e: '__', m: '_', v: '_' } 38 | const cn = withNaming(preset) 39 | 40 | cn('block', 'elem')({ theme: 'default' }) // -> block__elem_theme_default 41 | ``` 42 | -------------------------------------------------------------------------------- /packages/pack/src/cli/build.ts: -------------------------------------------------------------------------------- 1 | import { Command, Flags } from '@oclif/core' 2 | import { resolve } from 'path' 3 | import { loadConfig } from '../loadConfig' 4 | import { tryBuild } from '../build' 5 | 6 | type Flags = { config: string; silent: boolean } 7 | 8 | export default class Build extends Command { 9 | static description = 'Runs components build with defined plugins.' 10 | 11 | static flags = { 12 | config: Flags.string({ 13 | char: 'c', 14 | description: 'The path to a build config file.', 15 | default: 'build.config.js', 16 | }), 17 | silent: Flags.boolean({ 18 | description: 'Disable logs output.', 19 | }), 20 | } 21 | 22 | async run() { 23 | const { flags } = await this.parse(Build) 24 | const configs = await loadConfig(resolve(flags.config)) 25 | for (const config of configs) { 26 | tryBuild({ ...config, silent: flags.silent || config.silent }) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/classnames/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bem-react/classnames", 3 | "version": "1.4.0", 4 | "description": "BEM React ClassNames merge", 5 | "homepage": "https://github.com/bem/bem-react/tree/master/packages/classnames", 6 | "repository": "https://github.com/bem/bem-react", 7 | "keywords": ["classes", "merge"], 8 | "publishConfig": { 9 | "access": "public" 10 | }, 11 | "license": "MPL-2.0", 12 | "scripts": { 13 | "prepublishOnly": "npm run build", 14 | "build": "node ../../scripts/rollup/build.js", 15 | "unit": "../../node_modules/.bin/jest --config ../../.config/jest/jest.config.js" 16 | }, 17 | "files": ["build", "classnames.d.ts"], 18 | "main": "./build/classnames.cjs", 19 | "module": "./build/classnames.mjs", 20 | "types": "./classnames.d.ts", 21 | "exports": { 22 | ".": { 23 | "types": "./classnames.d.ts", 24 | "import": "./build/classnames.mjs", 25 | "require": "./build/classnames.cjs" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/classname/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bem-react/classname", 3 | "version": "1.7.0", 4 | "description": "BEM React ClassName", 5 | "homepage": "https://github.com/bem/bem-react/tree/master/packages/classname", 6 | "repository": "https://github.com/bem/bem-react", 7 | "keywords": ["bem", "naming", "classes", "notation", "core"], 8 | "publishConfig": { 9 | "access": "public" 10 | }, 11 | "license": "MPL-2.0", 12 | "scripts": { 13 | "prepublishOnly": "npm run build", 14 | "build": "node ../../scripts/rollup/build.js", 15 | "unit": "../../node_modules/.bin/jest --config ../../.config/jest/jest.config.js" 16 | }, 17 | "files": ["build", "classname.d.ts"], 18 | "main": "./build/classname.cjs", 19 | "module": "./build/classname.mjs", 20 | "types": "./classname.d.ts", 21 | "exports": { 22 | ".": { 23 | "types": "./classname.d.ts", 24 | "import": "./build/classname.mjs", 25 | "require": "./build/classname.cjs" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/webpack-exp-plugin/Readme.md: -------------------------------------------------------------------------------- 1 | # Webpack-exp-plugin 2 | 3 | Example - all components: 4 | 5 | ```js 6 | const webpackExpPlugin = require('@bem-react/webpack-exp-plugin') 7 | 8 | module.exports = { 9 | entry: './src/index.js', 10 | output: './dist', 11 | plugins: [new webpackExpPlugin('@yandex/ui', { '*': 'css-modules-exp' })], 12 | } 13 | ``` 14 | 15 | Example - Button only: 16 | 17 | ```js 18 | const webpackExpPlugin = require('@bem-react/webpack-exp-plugin') 19 | 20 | module.exports = { 21 | entry: './src/index.js', 22 | output: './dist', 23 | plugins: [new webpackExpPlugin('@yandex/ui', { Button: 'new-button-exp' })], 24 | } 25 | ``` 26 | 27 | Example - Button and Select: 28 | 29 | ```js 30 | const webpackExpPlugin = require('@bem-react/webpack-exp-plugin') 31 | 32 | module.exports = { 33 | entry: './src/index.js', 34 | output: './dist', 35 | plugins: [ 36 | new webpackExpPlugin('@yandex/ui', { Button: 'new-button-exp', Select: 'better-select-exp' }), 37 | ], 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /packages/di/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bem-react/di", 3 | "version": "5.1.0", 4 | "description": "BEM React Dependency Injection", 5 | "homepage": "https://github.com/bem/bem-react/tree/master/packages/di", 6 | "repository": "https://github.com/bem/bem-react", 7 | "keywords": ["bem", "level", "dependency", "di", "dependency injection", "react"], 8 | "publishConfig": { 9 | "access": "public" 10 | }, 11 | "license": "MPL-2.0", 12 | "scripts": { 13 | "prepublishOnly": "npm run build", 14 | "build": "node ../../scripts/rollup/build.js", 15 | "unit": "../../node_modules/.bin/jest --config ../../.config/jest/jest.config.js" 16 | }, 17 | "peerDependencies": { 18 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0" 19 | }, 20 | "files": ["build", "di.d.ts"], 21 | "main": "./build/di.cjs", 22 | "module": "./build/di.mjs", 23 | "types": "./di.d.ts", 24 | "exports": { 25 | ".": { 26 | "types": "./di.d.ts", 27 | "import": "./build/di.mjs", 28 | "require": "./build/di.cjs" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bem-react/core", 3 | "version": "5.2.0", 4 | "description": "BEM React Core", 5 | "homepage": "https://github.com/bem/bem-react/tree/master/packages/core", 6 | "repository": "https://github.com/bem/bem-react", 7 | "keywords": ["bem", "modifier", "withBemMod", "core"], 8 | "publishConfig": { 9 | "access": "public" 10 | }, 11 | "license": "MPL-2.0", 12 | "scripts": { 13 | "prepublishOnly": "npm run build", 14 | "build": "node ../../scripts/rollup/build.js", 15 | "unit": "../../node_modules/.bin/jest --config ../../.config/jest/jest.config.js" 16 | }, 17 | "dependencies": { 18 | "@bem-react/classname": "1.7.0", 19 | "@bem-react/classnames": "1.4.0" 20 | }, 21 | "peerDependencies": { 22 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0" 23 | }, 24 | "files": ["build", "core.d.ts"], 25 | "main": "./build/core.cjs", 26 | "module": "./build/core.mjs", 27 | "types": "./core.d.ts", 28 | "exports": { 29 | ".": { 30 | "types": "./core.d.ts", 31 | "import": "./build/core.mjs", 32 | "require": "./build/core.cjs" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/pack/src/plugins/PackageJsonPlugin.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, writeFileSync } from 'fs-extra' 2 | import { resolve } from 'path' 3 | import { Plugin, OnDone, HookOptions } from '../interfaces' 4 | import { mark } from '../debug' 5 | 6 | type Overrides = Record 7 | 8 | export class PackageJsonPlugin implements Plugin { 9 | name = 'PackageJsonPlugin' 10 | 11 | constructor(private overrides: Overrides) { 12 | mark('PackageJsonPlugin::constructor') 13 | } 14 | 15 | async onFinish(done: OnDone, { context, output }: HookOptions) { 16 | mark('PackageJsonPlugin::onFinish(start)') 17 | if (!existsSync(resolve(context, 'package.json'))) { 18 | throw new Error('Cannot find package.json.') 19 | } 20 | const pkg = require(resolve(context, 'package.json')) 21 | Object.assign(pkg, this.overrides) 22 | writeFileSync(resolve(output, 'package.json'), JSON.stringify(pkg, null, 2)) 23 | mark('PackageJsonPlugin::onFinish(finish)') 24 | done() 25 | } 26 | } 27 | 28 | export function usePackageJsonPlugin(overrides: Overrides): PackageJsonPlugin { 29 | return new PackageJsonPlugin(overrides) 30 | } 31 | -------------------------------------------------------------------------------- /packages/pack/src/interfaces.ts: -------------------------------------------------------------------------------- 1 | export type Config = { 2 | /** 3 | * Disable logs output. 4 | */ 5 | silent?: boolean 6 | 7 | /** 8 | * Executing context. 9 | * 10 | * @default cwd 11 | */ 12 | context?: string 13 | 14 | /** 15 | * Output directory. 16 | */ 17 | output: string 18 | 19 | /** 20 | * Plugins list. 21 | */ 22 | plugins: Plugin[] 23 | /** 24 | * Config name 25 | */ 26 | name?: string 27 | } 28 | 29 | export type OnDone = () => void 30 | export type HookOptions = { context: string; output: string } 31 | export type HookFn = (done: OnDone, options: HookOptions) => Promise 32 | 33 | export interface Plugin { 34 | /** 35 | * A plugin name. 36 | */ 37 | name: string 38 | 39 | /** 40 | * Run hook at start. 41 | */ 42 | onStart?: HookFn 43 | 44 | /** 45 | * Run hook before run. 46 | */ 47 | onBeforeRun?: HookFn 48 | 49 | /** 50 | * Run hook at run. 51 | */ 52 | onRun?: HookFn 53 | 54 | /** 55 | * Run hook after run. 56 | */ 57 | onAfterRun?: HookFn 58 | 59 | /** 60 | * Run hook at finish. 61 | */ 62 | onFinish?: HookFn 63 | } 64 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | unit: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: 18 20 | 21 | - name: install root-deps 22 | run: | 23 | npm i 24 | npm i -g lerna 25 | 26 | - name: build 27 | run: npm run build 28 | 29 | - name: units 30 | run: npm run unit 31 | env: 32 | CI: true 33 | 34 | linters: 35 | runs-on: ubuntu-latest 36 | 37 | steps: 38 | - uses: actions/checkout@v3 39 | with: 40 | fetch-depth: 0 41 | 42 | - uses: actions/setup-node@v3 43 | with: 44 | node-version: 18 45 | 46 | - name: install root-deps 47 | run: npm i --ignore-scripts 48 | 49 | - name: commitlint 50 | run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose 51 | 52 | - name: eslint 53 | run: npm run lint 54 | -------------------------------------------------------------------------------- /packages/eslint-plugin/README.md: -------------------------------------------------------------------------------- 1 | # @bem-react/eslint-plugin · [![npm (scoped)](https://img.shields.io/npm/v/@bem-react/eslint-plugin.svg)](https://www.npmjs.com/package/@bem-react/eslint-plugin) 2 | 3 | Plugin for checking some things in projects based on [BEM React](https://github.com/bem/bem-react). 4 | 5 | ## Usage 6 | 7 | Add `@bem-react` to the plugins section of your `.eslintrc` configuration file: 8 | 9 | ```json 10 | { 11 | "plugins": ["@bem-react"] 12 | } 13 | ``` 14 | 15 | Then configure the rules you want to use under the rules section. 16 | 17 | ```json 18 | { 19 | "rules": { 20 | "@bem-react/no-classname-runtime": "warn", 21 | "@bem-react/whitelist-levels-imports": [ 22 | "error", 23 | { 24 | "defaultLevel": "common", 25 | "whiteList": { 26 | "common": ["common"], 27 | "desktop": ["common", "desktop"], 28 | "mobile": ["common", "mobile"] 29 | } 30 | } 31 | ] 32 | } 33 | } 34 | ``` 35 | 36 | ## Supported Rules 37 | 38 | Currently is supported: 39 | 40 | - [whitelist-levels-imports](./docs/rules/whitelist-levels-imports.md) 41 | - [no-classname-runtime](./docs/rules/no-classname-runtime.md) 42 | -------------------------------------------------------------------------------- /website/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classnames from 'classnames' 3 | import Layout from '@theme/Layout' 4 | import Link from '@docusaurus/Link' 5 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext' 6 | import useBaseUrl from '@docusaurus/useBaseUrl' 7 | 8 | import styles from './styles.module.css' 9 | 10 | function Home() { 11 | const context = useDocusaurusContext() 12 | const { siteConfig = {} } = context 13 | return ( 14 | 15 |
16 |
17 |

{siteConfig.title}

18 |

{siteConfig.tagline}

19 |
20 | 27 | Get Started 28 | 29 |
30 |
31 |
32 |
33 | ) 34 | } 35 | 36 | export default Home 37 | -------------------------------------------------------------------------------- /packages/pack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bem-react/pack", 3 | "version": "3.1.0", 4 | "description": "A tool for building and prepare components for publishing", 5 | "homepage": "https://github.com/bem/bem-react/tree/master/packages/pack", 6 | "repository": "https://github.com/bem/bem-react", 7 | "keywords": [ 8 | "bem", 9 | "build", 10 | "react" 11 | ], 12 | "bin": { 13 | "pack": "bin/pack" 14 | }, 15 | "publishConfig": { 16 | "access": "public" 17 | }, 18 | "files": [ 19 | "bin", 20 | "lib" 21 | ], 22 | "license": "MPL-2.0", 23 | "scripts": { 24 | "build": "tsc", 25 | "cleanup": "rm -rf lib", 26 | "dev": "tsc -w", 27 | "prepublishOnly": "npm run cleanup && npm run build", 28 | "test": "exit 0" 29 | }, 30 | "dependencies": { 31 | "@oclif/core": "^1.21.0", 32 | "chalk": "4.1.0", 33 | "debug": "4.1.1", 34 | "fast-glob": "3.2.5", 35 | "fs-extra": "9.0.1", 36 | "log-update": "4.0.0", 37 | "tsc-alias": "1.8.6" 38 | }, 39 | "devDependencies": { 40 | "@babel/cli": "7.14.8", 41 | "@babel/core": "7.15.0", 42 | "@types/debug": "4.1.5", 43 | "@types/fs-extra": "9.0.1", 44 | "postcss": "7.0.32", 45 | "ts-node": "10.9.1", 46 | "typescript": "4.9.4" 47 | }, 48 | "oclif": { 49 | "commands": "./lib/cli", 50 | "bin": "pack" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/eslint-plugin/docs/rules/no-classname-runtime.md: -------------------------------------------------------------------------------- 1 | # @bem-react/no-classname-runtime 2 | 3 | Do not use the [@bem-react/classname](https://github.com/bem/bem-react/tree/master/packages/classname) function in runtime code 4 | 5 | ## Rule Details 6 | 7 | The classname method from [@bem-react/classname](https://github.com/bem/bem-react/tree/master/packages/classname) is often used and called in the hottest places, sometimes you can take it to the import level, then the result can be passed as a string literal directly to `className="x"` 8 | 9 | Examples of **incorrect** code for this rule: 10 | 11 | ```typescript jsx 12 | // … 13 | render() { 14 | return ( 15 | {items.map((item, idx, arr) => ( 16 |
17 |
18 | ItemText 19 |
20 |
21 | )} 22 | ) 23 | } 24 | ``` 25 | 26 | Examples of **correct** code for this rule: 27 | 28 | ```typescript jsx 29 | const cnItems = cn('Items'); 30 | const cnItem = cn('Item'); 31 | 32 | // … 33 | render() { 34 | return ( 35 | {items.map((item, idx, arr) => ( 36 |
37 |
38 | ItemText 39 |
40 |
41 | )} 42 | ) 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /website/docs/api/classname/cn.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: cn 3 | title: cn 4 | hide_title: true 5 | --- 6 | 7 | # `cn` 8 | 9 | Generate className for block or element with modifiers. 10 | 11 | #### Arguments 12 | 13 | 1. `blockName` (string): Block name. 14 | 2. `elementName`? (string): Element name. 15 | 16 | #### Returns 17 | 18 | `ClassNameFormatter` 19 | 20 | #### Examples 21 | 22 | Generate className for block: 23 | 24 | ```ts 25 | import { cn } from '@bem-react/classname' 26 | 27 | const buttonCn = cn('Button') 28 | 29 | buttonCn() // -> Button 30 | buttonCn({ size: 'm' }) // Button Button_size_m 31 | ``` 32 | 33 | Generate className for element: 34 | 35 | ```ts 36 | import { cn } from '@bem-react/classname' 37 | 38 | const buttonCn = cn('Button') 39 | 40 | buttonCn('Text') // -> Button-Text 41 | buttonCn('Text', { size: 'm' }) // Button-Text Button-Text_size_m 42 | ``` 43 | 44 | Generate className for element (alternative): 45 | 46 | ```ts 47 | import { cn } from '@bem-react/classname' 48 | 49 | const buttonTextCn = cn('Button', 'Text') 50 | 51 | buttonTextCn() // -> Button-Text 52 | buttonTextCn({ size: 'm' }) // Button-Text Button-Text_size_m 53 | ``` 54 | 55 | Generate className with additional mixin (**all duplicates will be removed**): 56 | 57 | ```ts 58 | import { cn } from '@bem-react/classname' 59 | 60 | const buttonCn = cn('Button') 61 | 62 | buttonCn(null, ['Header-Button']) // -> Button Header-Button 63 | ``` 64 | -------------------------------------------------------------------------------- /packages/pack/src/plugins/TypeScriptResolvePlugin.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process' 2 | import { promisify } from 'util' 3 | import { resolve } from 'path' 4 | import { existsSync } from 'fs-extra' 5 | 6 | import { Plugin, OnDone, HookOptions } from '../interfaces' 7 | import { mark } from '../debug' 8 | 9 | const execAsync = promisify(exec) 10 | 11 | type Options = { 12 | /** 13 | * A path to typescript config. 14 | */ 15 | configPath?: string 16 | } 17 | 18 | class TypeScriptResolvePlugin implements Plugin { 19 | name = 'TypescriptResolvePlugin' 20 | 21 | constructor(public options: Options = {} as Options) { 22 | mark('TypescriptResolvePlugin::constructor') 23 | } 24 | 25 | async onAfterRun(done: OnDone, { context }: HookOptions) { 26 | mark('TypescriptResolvePlugin::onRun(start)') 27 | const configPath = this.getConfigPath(context) 28 | 29 | try { 30 | await execAsync(`tsc-alias -p ${configPath}`) 31 | } catch (error) { 32 | throw new Error((error as any).stdout) 33 | } 34 | 35 | mark('TypescriptResolvePlugin::onRun(finish)') 36 | done() 37 | } 38 | 39 | private getConfigPath(context: string) { 40 | const configPath = resolve(context, this.options.configPath || 'tsconfig.json') 41 | 42 | if (!existsSync(configPath)) { 43 | throw new Error('Cannot find tsconfig.') 44 | } 45 | 46 | return configPath 47 | } 48 | } 49 | 50 | export function useTypeScriptResolvePlugin(options: Options) { 51 | return new TypeScriptResolvePlugin(options) 52 | } 53 | -------------------------------------------------------------------------------- /packages/classname/README.md: -------------------------------------------------------------------------------- 1 | # @bem-react/classname · [![npm (scoped)](https://img.shields.io/npm/v/@bem-react/classname.svg)](https://www.npmjs.com/package/@bem-react/classname) [![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/@bem-react/classname.svg)](https://bundlephobia.com/result?p=@bem-react/classname) 2 | 3 | Tiny helper for building CSS classes with BEM methodology. 4 | 5 | ## Install 6 | 7 | ``` 8 | npm i -S @bem-react/classname 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```js 14 | import { cn } from '@bem-react/classname' 15 | 16 | const cat = cn('Cat') 17 | 18 | cat() // Cat 19 | cat({ size: 'm' }) // Cat Cat_size_m 20 | cat('Tail') // Cat-Tail 21 | cat('Tail', { length: 'small' }) // Cat-Tail Cat-Tail_length_small 22 | 23 | const dogPaw = cn('Dog', 'Paw') 24 | 25 | dogPaw() // Dog-Paw 26 | dogPaw({ color: 'black', exists: true }) // Dog-Paw Dog-Paw_color_black Dog-Paw_exists 27 | 28 | // mixes 29 | 30 | cat(null, ['Dog']) // Cat Dog 31 | cat({ size: 'm' }, ['Dog', 'Horse']) // Cat Cat_size_m Dog Horse 32 | 33 | cat('Tail', [dogPaw()]) // Cat-Tail Dog-Paw 34 | cat('Tail', { length: 'small' }, [dogPaw({ color: 'black' })]) // Cat-Tail Cat-Tail_length_small Dog-Paw Dog-Paw_color_black 35 | ``` 36 | 37 | ## Configure 38 | 39 | By default `classname` uses React naming preset. But it's possible to use any. 40 | 41 | ```js 42 | import { withNaming } from '@bem-react/classname' 43 | 44 | const cn = withNaming({ n: 'ns-', e: '__', m: '_', v: '_' }) 45 | 46 | cn('block', 'elem')({ theme: 'default' }) // ns-block__elem_theme_default 47 | ``` 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "bootstrap": "lerna bootstrap --no-ci", 5 | "build": "lerna run build --concurrency=1", 6 | "lint": "eslint --ext .js,.cjs,.ts,.tsx .", 7 | "postinstall": "npm run bootstrap", 8 | "publish:next": "lerna publish --canary --preid dev --npm-tag next --no-git-tag-version", 9 | "unit:coverage": "npm run unit -- --coverage", 10 | "unit": "jest --config .config/jest/jest.config.js" 11 | }, 12 | "devDependencies": { 13 | "@commitlint/cli": "17.3.0", 14 | "@commitlint/config-conventional": "17.3.0", 15 | "@types/jest": "26.0.14", 16 | "@types/react": "18.0.26", 17 | "@typescript-eslint/eslint-plugin": "2.22.0", 18 | "@typescript-eslint/parser": "2.22.0", 19 | "chalk": "2.4.1", 20 | "eslint": "6.8.0", 21 | "eslint-plugin-react": "7.19.0", 22 | "eslint-plugin-react-hooks": "2.5.0", 23 | "gzip-size": "5.1.0", 24 | "husky": "3.0.5", 25 | "jest": "29.3.1", 26 | "jest-environment-jsdom": "29.3.1", 27 | "@testing-library/react": "13.4.0", 28 | "lerna": "3.5.1", 29 | "lint-staged": "9.2.5", 30 | "prettier": "2.8.1", 31 | "pretty-bytes": "5.2.0", 32 | "react": "18.2.0", 33 | "react-dom": "18.2.0", 34 | "rollup": "1.10.1", 35 | "rollup-plugin-node-resolve": "4.2.3", 36 | "rollup-plugin-replace": "2.1.0", 37 | "rollup-plugin-strip-banner": "0.2.0", 38 | "rollup-plugin-terser": "4.0.4", 39 | "rollup-plugin-typescript2": "0.21.0", 40 | "ts-jest": "29.0.3", 41 | "typescript": "4.9.4", 42 | "tslib": "2.4.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/eslint-plugin/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.1.2](https://github.com/bem/bem-react/compare/@bem-react/eslint-plugin@1.1.1...@bem-react/eslint-plugin@1.1.2) (2020-03-12) 7 | 8 | **Note:** Version bump only for package @bem-react/eslint-plugin 9 | 10 | ## [1.1.1](https://github.com/bem/bem-react/compare/@bem-react/eslint-plugin@1.1.0...@bem-react/eslint-plugin@1.1.1) (2019-10-20) 11 | 12 | ### Bug Fixes 13 | 14 | - **eslint-plugin:** case for destructuring ([e4e27e8](https://github.com/bem/bem-react/commit/e4e27e8)) 15 | 16 | # [1.1.0](https://github.com/bem/bem-react/compare/@bem-react/eslint-plugin@1.0.3...@bem-react/eslint-plugin@1.1.0) (2019-10-14) 17 | 18 | ### Features 19 | 20 | - **eslint-plugin:** added new plugin ([0fbb7ad](https://github.com/bem/bem-react/commit/0fbb7ad)) 21 | - **eslint-plugin:** fix reporting ([b9fcf02](https://github.com/bem/bem-react/commit/b9fcf02)) 22 | - **eslint-plugin:** review ([57737f1](https://github.com/bem/bem-react/commit/57737f1)) 23 | 24 | ## [1.0.3](https://github.com/bem/bem-react/compare/@bem-react/eslint-plugin@1.0.2...@bem-react/eslint-plugin@1.0.3) (2019-10-02) 25 | 26 | ### Bug Fixes 27 | 28 | - **eslint-plugin:** update eslint dependency ([1e92b3a](https://github.com/bem/bem-react/commit/1e92b3a)) 29 | 30 | ## [1.0.2](https://github.com/bem/bem-react/compare/@bem-react/eslint-plugin@1.0.1...@bem-react/eslint-plugin@1.0.2) (2019-08-20) 31 | 32 | **Note:** Version bump only for package @bem-react/eslint-plugin 33 | 34 | ## [1.0.1](https://github.com/bem/bem-react/compare/@bem-react/eslint-plugin@1.0.0...@bem-react/eslint-plugin@1.0.1) (2019-07-31) 35 | 36 | **Note:** Version bump only for package @bem-react/eslint-plugin 37 | -------------------------------------------------------------------------------- /website/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/pack/src/plugins/CopyAssetsPlugin.ts: -------------------------------------------------------------------------------- 1 | import { copyFile, ensureDir } from 'fs-extra' 2 | import { resolve, dirname } from 'path' 3 | import glob from 'fast-glob' 4 | import { Plugin, OnDone, HookOptions } from '../interfaces' 5 | import { mark } from '../debug' 6 | 7 | type Rule = { 8 | /** 9 | * A path that determines how to interpret the `src` path. 10 | */ 11 | context?: string 12 | 13 | /** 14 | * Glob or path from where we сopy files. 15 | */ 16 | src: string 17 | 18 | /** 19 | * Output paths. 20 | */ 21 | output: string[] 22 | 23 | /** 24 | * Paths to files that will be ignored when copying. 25 | */ 26 | ignore?: string[] 27 | } 28 | 29 | type Rules = Rule | Rule[] 30 | 31 | export class CopyAssetsPlugin implements Plugin { 32 | name = 'CopyAssetsPlugin' 33 | 34 | constructor(public rules: Rules) { 35 | mark('CopyAssetsPlugin::constructor') 36 | } 37 | 38 | async onAfterRun(done: OnDone, { context, output }: HookOptions) { 39 | mark('CopyAssetsPlugin::onAfterRun(start)') 40 | const rules = Array.isArray(this.rules) ? this.rules : [this.rules] 41 | for (const rule of rules) { 42 | const ctx = rule.context ? resolve(context, rule.context) : context 43 | const files = await glob(rule.src, { cwd: ctx, ignore: rule.ignore }) 44 | for (const file of files) { 45 | const dirs = rule.output ? rule.output : [output] 46 | for (const dir of dirs) { 47 | const destPath = resolve(context, dir, file) 48 | await ensureDir(dirname(destPath)) 49 | await copyFile(resolve(ctx, file), destPath) 50 | } 51 | } 52 | } 53 | mark('CopyAssetsPlugin::onAfterRun(finish)') 54 | done() 55 | } 56 | } 57 | 58 | export function useCopyAssetsPlugin(rules: Rules): CopyAssetsPlugin { 59 | return new CopyAssetsPlugin(rules) 60 | } 61 | -------------------------------------------------------------------------------- /website/docs/guides/structure.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: structure 3 | title: Component structure 4 | --- 5 | 6 | In common cases the basic structure of the component looks something like this: 7 | 8 | ``` 9 | src/components/Button/ 10 | ├── Button.css 11 | ├── Button.tsx 12 | ├── Button@desktop.css 13 | ├── Button@desktop.tsx 14 | ├── desktop.ts 15 | ├── _view 16 | │ ├── Button_view_default.css 17 | │   └── Button_view_default.tsx 18 | └── hooks 19 | └── useCheckedState.ts 20 | ``` 21 | 22 | ## Platforms 23 | 24 | If a component has some platform-specific features, such as hover state, then you need to create an implementation for that platform and reexport all from common. 25 | 26 | Common implementation: 27 | 28 | ```tsx 29 | // src/components/Button/Button.tsx 30 | import React from 'react' 31 | import './Button.css' 32 | 33 | export const Button = ({ children }) => 34 | ``` 35 | 36 | Desktop implementation: 37 | 38 | ```tsx 39 | // src/components/Button/Button@desktop.tsx 40 | export * from './Button' 41 | import './Button@desktop.css' 42 | ``` 43 | 44 | ## Public API 45 | 46 | The public API allows you to control what should be available to the developer, and also encapsulates the logic about the absence of any code for a particular platform. 47 | 48 | > Very important that the project was configured with [tree shaking](https://webpack.js.org/guides/tree-shaking/), otherwise in result bundle may be unused code. 49 | 50 | Example: 51 | 52 | ```ts 53 | // src/components/Button/desktop.ts 54 | export * from './Button@desktop' 55 | export * from './_view/Button_view_default' 56 | export * from './hooks/useCheckedState' 57 | ``` 58 | 59 | Usage: 60 | 61 | ```ts 62 | // src/components/Feature/Feature.tsx 63 | import { 64 | Button as ButtonDesktop, 65 | withViewDefault, 66 | useCheckedState, 67 | } from 'components/Button/desktop' 68 | ``` 69 | -------------------------------------------------------------------------------- /packages/eslint-plugin/tests/lib/rules/no-classname-runtime.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../../../lib/rules/no-classname-runtime') 4 | const { RuleTester } = require('eslint') 5 | 6 | RuleTester.setDefaultConfig({ 7 | parserOptions: { 8 | ecmaVersion: 2015, 9 | sourceType: 'module', 10 | ecmaFeatures: { 11 | jsx: true, 12 | }, 13 | }, 14 | }) 15 | 16 | const ruleTester = new RuleTester() 17 | 18 | ruleTester.run('no-classname-runtime', rule, { 19 | valid: [ 20 | '', 21 | '', 22 | '', 23 | '', 24 | '', 25 | '', 26 | '', 27 | '', 28 | ].map((code) => ({ 29 | code, 30 | })), 31 | 32 | invalid: [ 33 | '', 34 | '', 35 | '', 36 | '', 37 | '', 38 | '', 39 | '', 40 | '', 41 | '', 42 | ' b })}/>', 43 | '', 44 | '', 45 | '', 46 | ].map((code) => ({ 47 | code, 48 | errors: [ 49 | { 50 | message: "You can speed up your code if you don't call the function on every render", 51 | }, 52 | ], 53 | })), 54 | }) 55 | -------------------------------------------------------------------------------- /website/docs/guides/lazy.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: lazy 3 | title: Lazy modifiers 4 | --- 5 | 6 | Solution for better code spliting with React.lazy and dynamic imports 7 | 8 | ## Structure 9 | 10 | ``` 11 | src/components/Block/ 12 | ├── Block.tsx 13 | ├── _mod 14 | │   └── Button_mod.async.tsx 15 | │   └── Button_mod.css 16 | │   └── Button_mod.tsx 17 | ``` 18 | 19 | ## Declaration 20 | 21 | **Step 1.** Declare the part of the code that should be loaded dynamically. 22 | 23 | ```tsx 24 | // Block/_mod/Block_mod.async.tsx 25 | import React from 'react' 26 | import { cnBlock } from '../Block' 27 | 28 | import './Block_mod.css' 29 | 30 | export const DynamicPart: React.FC = () => Loaded dynamicly 31 | 32 | // defualt export needed for React.lazy 33 | export default DynamicPart 34 | ``` 35 | 36 | **Step 2.** Declare the modifier. 37 | 38 | ```tsx 39 | // Block/_mod/Block_mod.tsx 40 | import React, { Suspense, lazy } from 'react' 41 | import { cnBlock } from '../Block' 42 | 43 | export interface BlockModProps { 44 | mod?: boolean 45 | } 46 | 47 | export const withMod = withBemMod( 48 | cnBlock(), 49 | { 50 | mod: true, 51 | }, 52 | (Block) => (props) => { 53 | const DynamicPart = lazy(() => import('./Block_mod.async.tsx')) 54 | 55 | return ( 56 | Updating...}> 57 | 58 | 59 | 60 | 61 | ) 62 | }, 63 | ) 64 | ``` 65 | 66 | > **NOTE** If your need SSR replace React.lazy method for load `Block_mod.async.tsx` module to [@loadable/components](https://www.smooth-code.com/open-source/loadable-components/) or [react-loadable](https://github.com/jamiebuilds/react-loadable) 67 | 68 | ## Usage 69 | 70 | ```ts 71 | // App.tsx 72 | import { 73 | Block as BlockPresenter, 74 | withMod 75 | } from './components/Block/desktop'; 76 | 77 | const Block = withMod(BlockPresenter); 78 | 79 | export const App = () => { 80 | return ( 81 | {/* chunk with DynamicPart not loaded */} 82 | 83 | 84 | {/* chunk with DynamicPart loaded */} 85 | 86 | ) 87 | } 88 | ``` 89 | -------------------------------------------------------------------------------- /packages/webpack-exp-plugin/replacer.test.js: -------------------------------------------------------------------------------- 1 | const { it, describe } = require('@jest/globals') 2 | const replacer = require('./replacer') 3 | 4 | describe('replacer', () => { 5 | it('all matcher', async () => { 6 | const importPath = '@yandex-lego/components/Button/desktop' 7 | 8 | expect(replacer('@yandex-lego/components', { '*': 'css-modules' }, importPath)).toBe( 9 | '@yandex-lego/components/experiments/css-modules/Button/desktop', 10 | ) 11 | }) 12 | 13 | it('button only', async () => { 14 | const config = { Button: 'css-modules' } 15 | 16 | expect( 17 | replacer('@yandex-lego/components', config, '@yandex-lego/components/Button/desktop'), 18 | ).toBe('@yandex-lego/components/experiments/css-modules/Button/desktop') 19 | 20 | expect( 21 | replacer('@yandex-lego/components', config, '@yandex-lego/components/Select/desktop'), 22 | ).toBe('@yandex-lego/components/Select/desktop') 23 | }) 24 | 25 | it('few experiments', async () => { 26 | const config = { 27 | Button: 'css-modules', 28 | Select: 'good-select', 29 | } 30 | 31 | expect( 32 | replacer('@yandex-lego/components', config, '@yandex-lego/components/Button/desktop'), 33 | ).toBe('@yandex-lego/components/experiments/css-modules/Button/desktop') 34 | 35 | expect( 36 | replacer('@yandex-lego/components', config, '@yandex-lego/components/Select/desktop'), 37 | ).toBe('@yandex-lego/components/experiments/good-select/Select/desktop') 38 | }) 39 | 40 | it('first only', async () => { 41 | const config = { 42 | '*': 'make-lego-great-again', 43 | Button: 'css-modules', 44 | } 45 | 46 | expect( 47 | replacer('@yandex-lego/components', config, '@yandex-lego/components/Button/desktop'), 48 | ).toBe('@yandex-lego/components/experiments/make-lego-great-again/Button/desktop') 49 | }) 50 | 51 | it('skip if experiment', async () => { 52 | const config = { '*': 'css-modules' } 53 | 54 | expect( 55 | replacer( 56 | '@yandex-lego/components', 57 | config, 58 | '@yandex-lego/components/experiments/some/Button/desktop', 59 | ), 60 | ).toBe('@yandex-lego/components/experiments/some/Button/desktop') 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /website/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'BEM React', 3 | tagline: 'A set of tools for developing user interfaces using the BEM methodology in React', 4 | url: 'https://bem.github.io', 5 | baseUrl: '/bem-react/', 6 | favicon: 'img/favicon.ico', 7 | organizationName: 'bem', 8 | projectName: 'bem-react', 9 | themeConfig: { 10 | disableDarkMode: true, 11 | prism: { 12 | theme: require('prism-react-renderer/themes/github'), 13 | }, 14 | navbar: { 15 | title: 'BEM React', 16 | logo: { 17 | alt: 'bem react Logo', 18 | src: 'img/logo.svg', 19 | }, 20 | links: [ 21 | { to: 'docs/introduction/installation', label: 'Docs', position: 'left' }, 22 | { to: 'docs/api/core/core', label: 'API', position: 'left' }, 23 | { 24 | href: 'https://github.com/bem/bem-react', 25 | label: 'GitHub', 26 | position: 'right', 27 | }, 28 | ], 29 | }, 30 | footer: { 31 | style: 'light', 32 | links: [ 33 | { 34 | title: 'Docs', 35 | items: [ 36 | { 37 | label: 'Get started', 38 | to: 'docs/introduction/installation', 39 | }, 40 | { 41 | label: 'API', 42 | to: 'docs/api/core/core', 43 | }, 44 | ], 45 | }, 46 | { 47 | title: 'Community', 48 | items: [ 49 | { 50 | label: 'Telegram', 51 | href: 'https://t.me/bem_ru', 52 | }, 53 | ], 54 | }, 55 | { 56 | title: 'More', 57 | items: [ 58 | { 59 | label: 'BEM Info', 60 | href: 'https://bem.info', 61 | }, 62 | { 63 | label: 'GitHub', 64 | href: 'https://github.com/bem/bem-react', 65 | }, 66 | ], 67 | }, 68 | ], 69 | copyright: `Copyright © ${new Date().getFullYear()} BEM`, 70 | }, 71 | }, 72 | presets: [ 73 | [ 74 | '@docusaurus/preset-classic', 75 | { 76 | docs: { 77 | sidebarPath: require.resolve('./sidebars.js'), 78 | editUrl: 'https://github.com/bem/bem-react/edit/master/website/', 79 | }, 80 | theme: { 81 | customCss: require.resolve('./src/css/custom.css'), 82 | }, 83 | }, 84 | ], 85 | ], 86 | } 87 | -------------------------------------------------------------------------------- /packages/pack/src/progress.ts: -------------------------------------------------------------------------------- 1 | import { create as createRenderer } from 'log-update' 2 | import c from 'chalk' 3 | 4 | export const enum States { 5 | inqueue = 'INQUEUE', 6 | running = 'RUNNING', 7 | done = 'DONE', 8 | failed = 'FAILED', 9 | } 10 | 11 | // TODO: Disable keypress from stdin. 12 | export function renderProgressState(state: State, silent: boolean = false): () => void { 13 | if (silent) { 14 | return () => {} 15 | } 16 | 17 | function renderFn() { 18 | updateProgressFrame(state) 19 | renderLog(createStateView(state)) 20 | } 21 | 22 | const intervalRef = setInterval(renderFn, 125) 23 | 24 | return () => { 25 | clearInterval(intervalRef) 26 | renderFn() 27 | } 28 | } 29 | 30 | const renderLog = createRenderer(process.stdout, { showCursor: true }) 31 | 32 | type State = Record< 33 | string, 34 | { 35 | name: string 36 | step: string 37 | state: string 38 | value: string 39 | time: string | null 40 | frame: string 41 | } 42 | > 43 | 44 | const stateColors: Record = { 45 | [States.inqueue]: 'gray', 46 | [States.running]: 'yellow', 47 | [States.done]: 'green', 48 | [States.failed]: 'red', 49 | } 50 | 51 | function createSeparator(state: State) { 52 | const values = [] 53 | for (const [_, value] of Object.entries(state)) { 54 | values.push((value.name + value.step + '....').length) 55 | } 56 | const maxValue = Math.max(...values) 57 | return (a: string, b: string) => { 58 | return '.'.repeat(maxValue - (a + b).length) 59 | } 60 | } 61 | 62 | function createStateView(state: State): string { 63 | const sep = createSeparator(state) 64 | const result = [] 65 | for (const [_, value] of Object.entries(state)) { 66 | const message = [ 67 | `${c.gray('[@bem-react/pack]')}`, 68 | `${c.cyan(value.name)}:${value.step}`, 69 | `${c.gray(sep(value.name, value.step))}`, 70 | `[${(c as any)[stateColors[value.state]](value.state)}]`, 71 | ] 72 | 73 | if (value.state !== 'INQUEUE') { 74 | message.push(`${c.gray(`(${value.time ? value.time : value.frame})`)}`) 75 | } 76 | 77 | result.push(message.join(' ')) 78 | } 79 | return result.join('\n') 80 | } 81 | 82 | function createProgressFrame() { 83 | let i = 0 84 | const frames = ['∙∙∙', '●∙∙', '∙●∙', '∙∙●', '∙∙∙'] 85 | return () => frames[++i % frames.length] 86 | } 87 | 88 | const getNextFrame = createProgressFrame() 89 | 90 | function updateProgressFrame(state: State): void { 91 | for (const [_, value] of Object.entries(state)) { 92 | value.frame = getNextFrame() 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Bem React Core is an open source library that is under active development and is also used within [Yandex](https://yandex.com/company/). 4 | 5 | All work on Bem React Core is done directly on GitHub. Members of the core group as well other participants send [Pull Requests](https://github.com/bem/bem-react-core/pulls) that go through the same verification process. 6 | 7 | Development is carried out in branches. The main branch is `master`. The code in the `master`branch has been tested and is recommended for use. 8 | 9 | To make changes: 10 | 11 | 1. [Create an issue](#creating-an-issue) 12 | 2. [Send your Pull Request](#sending-a-pull-request) 13 | 14 | ## Creating an issue 15 | 16 | If you found a bug or want to make an improvement in the API: 17 | 18 | 1. First check whether the same issue already exists in the [list of issues](https://github.com/bem/bem-react-core/issues). 19 | 2. If you don't find the issue there, [create a new one](https://github.com/bem/bem-react-core/issues/new) including a description of the problem. 20 | 21 | > **Note:** Languages other than English are not normally used in issue descriptions. 22 | 23 | ## Sending a Pull Request 24 | 25 | To make changes to the library: 26 | 27 | 1. Fork the repository. 28 | 2. Clone the fork. 29 | 30 | ```bash 31 | $ git clone git@github.com:/bem-react-core.git 32 | ``` 33 | 34 | 3. Add the main repository for the `bem-react-core` library as a remote repository with the name "upstream". 35 | 36 | ```bash 37 | $ cd bem-react-core 38 | $ git remote add upstream git@github.com:bem/bem-react-core.git 39 | ``` 40 | 41 | 4. Fetch the latest changes. 42 | 43 | ```bash 44 | $ git fetch upstream 45 | ``` 46 | 47 | > **Note:** Repeat this step before every change you make, to be sure that you are working with code that contains the latest updates. 48 | 49 | 5. Create a `feature-branch` that includes the number of the [created issue](#creating-an-issue). 50 | 51 | ```bash 52 | $ git checkout upstream/master 53 | $ git checkout -b issue- 54 | ``` 55 | 56 | 6. Make changes. 57 | 7. Record the changes made by making comments in accordance with [Conventional Commits](https://conventionalcommits.org). 58 | 59 | ```bash 60 | $ git commit -m "[optional scope]: " 61 | ``` 62 | 63 | 8. Fetch the latest changes. 64 | 65 | ```bash 66 | $ git pull --rebase upstream master 67 | ``` 68 | 69 | 9. Send the changes to GitHub. 70 | 71 | ```bash 72 | $ git push -u origin issue- 73 | ``` 74 | 75 | 10. Send a [Pull Request](https://github.com/bem/bem-react-core/compare) based on the branch created. 76 | 11. Link the Pull Request and issue (for example, with a comment). 77 | 12. Wait for a decision about accepting the changes. 78 | -------------------------------------------------------------------------------- /packages/pack/src/build.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import c from 'chalk' 3 | 4 | import { Config, HookOptions } from './interfaces' 5 | import { wrapToPromise } from './wrapToPromise' 6 | import { measurePerf } from './measurePerf' 7 | import { States, renderProgressState } from './progress' 8 | import { stdout } from './stdout' 9 | 10 | const steps = ['onStart', 'onBeforeRun', 'onRun', 'onAfterRun', 'onFinish'] 11 | 12 | export async function tryBuild(config: Config): Promise { 13 | config.context = config.context || process.cwd() 14 | const options: HookOptions = { 15 | context: config.context, 16 | output: resolve(config.context, config.output), 17 | } 18 | 19 | const errors = [] 20 | const getTotalPerf = measurePerf() 21 | const state = createProgressState(config, steps) 22 | const disposeRender = renderProgressState(state, config.silent) 23 | 24 | process.on('SIGINT', async () => { 25 | disposeRender() 26 | 27 | // Run onAfterRun (cleanup) hook when process is interrupted. 28 | for (const plugin of config.plugins) { 29 | const hook = plugin.onAfterRun 30 | if (hook !== undefined) { 31 | await wrapToPromise(hook.bind(plugin), options) 32 | } 33 | } 34 | }) 35 | 36 | stdout.plain(`${c.gray('[@bem-react/pack]')} Start building...`) 37 | for (const step of steps) { 38 | for (const plugin of config.plugins) { 39 | const hook = (plugin as any)[step] 40 | if (hook === undefined) { 41 | continue 42 | } 43 | const getStepPerf = measurePerf() 44 | state[plugin.name + step].state = States.running 45 | try { 46 | await wrapToPromise(hook.bind(plugin), options) 47 | state[plugin.name + step].state = States.done 48 | } catch (error) { 49 | errors.push(error) 50 | state[plugin.name + step].state = States.failed 51 | } finally { 52 | state[plugin.name + step].time = getStepPerf() 53 | } 54 | } 55 | } 56 | const time = getTotalPerf() 57 | disposeRender() 58 | if (errors.length > 0) { 59 | for (const error of errors) { 60 | stdout.error(c.red(error)) 61 | } 62 | stdout.error(`${c.gray('[@bem-react/pack]')} ${c.red('Building failed!')} (${c.green(time)})`) 63 | } else { 64 | // prettier-ignore 65 | stdout.plain(`${c.gray('[@bem-react/pack]')} ${c.green('Building complete!')} (${c.green(time)})`) 66 | } 67 | } 68 | 69 | function createProgressState(config: Config, steps: string[]) { 70 | const state: Record = {} 71 | for (const plugin of config.plugins) { 72 | for (const step of steps) { 73 | const hook = (plugin as any)[step] 74 | if (hook !== undefined) { 75 | state[plugin.name + step] = { step, name: plugin.name, state: States.inqueue, time: null } 76 | } 77 | } 78 | } 79 | return state 80 | } 81 | -------------------------------------------------------------------------------- /packages/pack/src/plugins/CssPlugin.ts: -------------------------------------------------------------------------------- 1 | import { resolve, dirname } from 'path' 2 | import { ensureDir, readFile, writeFile, existsSync } from 'fs-extra' 3 | import glob from 'fast-glob' 4 | import postcss, { Processor } from 'postcss' 5 | 6 | import { Plugin, OnDone, HookOptions } from '../interfaces' 7 | import { mark } from '../debug' 8 | 9 | type Options = { 10 | /** 11 | * A path that determines how to interpret the `src` path. 12 | */ 13 | context?: string 14 | 15 | /** 16 | * Glob or path from where we сopy files. 17 | */ 18 | src: string 19 | 20 | /** 21 | * Output paths. 22 | */ 23 | output: string[] 24 | 25 | /** 26 | * Paths to files that will be ignored when copying and processing. 27 | */ 28 | ignore?: string[] 29 | 30 | /** 31 | * A path to postcss config. 32 | */ 33 | postcssConfigPath?: string 34 | } 35 | 36 | class CssPlugin implements Plugin { 37 | name = 'CssPlugin' 38 | 39 | constructor(public options: Options) { 40 | mark('CssPlugin::constructor') 41 | } 42 | 43 | async onRun(done: OnDone, { context, output }: HookOptions) { 44 | mark('CssPlugin::onRun(start)') 45 | const ctx = this.options.context ? resolve(context, this.options.context) : context 46 | const processor = this.getCssProcessor(context) 47 | const files = await glob(this.options.src, { cwd: ctx, ignore: this.options.ignore }) 48 | for (const file of files) { 49 | const rawContent = await readFile(resolve(ctx, file), 'utf-8') 50 | const content = await this.runCssProcessorIfAvailable(rawContent, processor) 51 | const dirs = this.options.output ? this.options.output : [output] 52 | for (const dir of dirs) { 53 | const destPath = resolve(context, dir, file) 54 | await ensureDir(dirname(destPath)) 55 | await writeFile(destPath, content) 56 | } 57 | } 58 | mark('CssPlugin::onRun(finish)') 59 | done() 60 | } 61 | 62 | private runCssProcessorIfAvailable(rawContent: string, processor?: Processor) { 63 | if (this.availableCssProcessor(processor)) { 64 | return processor.process(rawContent, { from: '', to: '' }).then((result) => result.css) 65 | } 66 | 67 | return Promise.resolve(rawContent) 68 | } 69 | 70 | private availableCssProcessor(_p?: Processor): _p is Processor { 71 | return this.options.postcssConfigPath !== undefined 72 | } 73 | 74 | private getCssProcessor(context: string): Processor | undefined { 75 | if (this.options.postcssConfigPath === undefined) { 76 | return undefined 77 | } 78 | const configPath = resolve(context, this.options.postcssConfigPath) 79 | if (!existsSync(configPath)) { 80 | throw new Error('Cannot find potcss config.') 81 | } 82 | const config = require(configPath) 83 | return postcss(config.plugins) 84 | } 85 | } 86 | 87 | export function useCssPlugin(options: Options): CssPlugin { 88 | return new CssPlugin(options) 89 | } 90 | -------------------------------------------------------------------------------- /packages/pack/src/plugins/TypeScriptPlugin.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process' 2 | import { promisify } from 'util' 3 | import { resolve, join, dirname, relative } from 'path' 4 | import { existsSync, writeJson } from 'fs-extra' 5 | import glob from 'fast-glob' 6 | 7 | import { Plugin, OnDone, HookOptions } from '../interfaces' 8 | import { mark } from '../debug' 9 | 10 | const execAsync = promisify(exec) 11 | 12 | type Options = { 13 | /** 14 | * A path to typescript config. 15 | */ 16 | configPath?: string 17 | /** 18 | * A callback for when creating side effects. 19 | */ 20 | onCreateSideEffects: (path: string) => string[] | boolean | undefined 21 | } 22 | 23 | class TypeScriptPlugin implements Plugin { 24 | name = 'TypeScriptPlugin' 25 | 26 | constructor(public options: Options = {} as Options) { 27 | mark('TypeScriptPlugin::constructor') 28 | } 29 | 30 | async onRun(done: OnDone, { context, output }: HookOptions) { 31 | mark('TypeScriptPlugin::onRun(start)') 32 | const configPath = this.getConfigPath(context) 33 | try { 34 | await Promise.all([ 35 | // prettier-ignore 36 | execAsync(`npx tsc -p ${configPath} --module commonjs --outDir ${output}`), 37 | // prettier-ignore 38 | execAsync(`npx tsc -p ${configPath} --module esnext --outDir ${resolve(output, 'esm')}`), 39 | ]) 40 | } catch (error) { 41 | throw new Error((error as any).stdout) 42 | } 43 | await this.generateModulePackage(output) 44 | mark('TypeScriptPlugin::onRun(finish)') 45 | done() 46 | } 47 | 48 | private getConfigPath(context: string): string { 49 | const configPath = resolve(context, this.options.configPath || 'tsconfig.json') 50 | if (!existsSync(configPath)) { 51 | throw new Error('Cannot find tsconfig.') 52 | } 53 | return configPath 54 | } 55 | 56 | // TODO: Move this logic to separate plugin. 57 | private async generateModulePackage(src: string): Promise { 58 | const files = await glob('**/index.js', { cwd: src }) 59 | for (const file of files) { 60 | const moduleDirname = dirname(file) 61 | const esmModuleDirname = dirname(join('esm', file)) 62 | const packageJsonPath = resolve(src, moduleDirname, 'package.json') 63 | const json: { sideEffects: string[] | boolean; module?: string } = { 64 | sideEffects: ['*.css', '*@desktop.js', '*@touch-phone.js', '*@touch-pad.js'], 65 | } 66 | 67 | if (this.options.onCreateSideEffects !== undefined) { 68 | const sideEffects = this.options.onCreateSideEffects(file) 69 | if (sideEffects !== undefined) { 70 | json.sideEffects = sideEffects 71 | } 72 | } 73 | 74 | if (file.match(/^esm/) === null) { 75 | json.module = join(relative(moduleDirname, esmModuleDirname), 'index.js') 76 | } 77 | 78 | await writeJson(packageJsonPath, json, { spaces: 2 }) 79 | } 80 | } 81 | } 82 | 83 | export function useTypeScriptPlugin(options: Options): TypeScriptPlugin { 84 | return new TypeScriptPlugin(options) 85 | } 86 | -------------------------------------------------------------------------------- /packages/eslint-plugin/lib/rules/whitelist-levels-imports.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | 5 | const LEVEL_REGEXP = '^[\\w-]+$' 6 | 7 | module.exports = { 8 | meta: { 9 | type: 'problem', 10 | schema: [ 11 | { 12 | type: 'object', 13 | properties: { 14 | defaultLevel: { 15 | type: 'string', 16 | pattern: LEVEL_REGEXP, 17 | }, 18 | whiteList: { 19 | type: 'object', 20 | minProperties: 1, 21 | patternProperties: { 22 | [LEVEL_REGEXP]: { 23 | type: 'array', 24 | minItems: 1, 25 | items: { 26 | type: 'string', 27 | pattern: LEVEL_REGEXP, 28 | }, 29 | }, 30 | }, 31 | }, 32 | ignorePaths: { 33 | type: 'array', 34 | minItems: 1, 35 | items: { 36 | type: 'string', 37 | }, 38 | }, 39 | }, 40 | required: ['defaultLevel', 'whiteList'], 41 | }, 42 | ], 43 | }, 44 | 45 | create: function(context) { 46 | const filepath = context.getFilename() 47 | const { whiteList, defaultLevel, ignorePaths } = context.options[0] 48 | 49 | /** 50 | * @param {String} filepath 51 | * 52 | * @returns {String|null} 53 | */ 54 | function getLevelFromFilename(filepath) { 55 | const ext = path.extname(filepath) 56 | const basename = path.basename(filepath, ext) 57 | 58 | // import from 'foo/touch-phone' 59 | if (whiteList[basename]) { 60 | return basename 61 | } 62 | 63 | // import from './foo@touch-phone' 64 | // import from './foo@touch-phone.example' 65 | const found = basename.match(/@([\w-]+)/) 66 | 67 | return (found && found[1]) || null 68 | } 69 | 70 | /** 71 | * @param {String} filepath 72 | * 73 | * @returns {Boolean} 74 | */ 75 | function shouldIgnoreFile(filepath) { 76 | return ( 77 | Array.isArray(ignorePaths) && ignorePaths.some((path) => new RegExp(path).test(filepath)) 78 | ) 79 | } 80 | 81 | if (shouldIgnoreFile(filepath)) { 82 | return {} 83 | } 84 | 85 | const fileLevel = getLevelFromFilename(filepath) || defaultLevel 86 | const allowedLevels = whiteList[fileLevel] || [] 87 | 88 | return { 89 | ImportDeclaration: function(node) { 90 | const importPath = node.source.value 91 | const importLevel = getLevelFromFilename(importPath) 92 | 93 | if (importLevel && !allowedLevels.includes(importLevel)) { 94 | context.report({ 95 | node: node, 96 | message: 97 | "Imports from '{{ importLevel }}' level in files from '{{ fileLevel }}' level are forbidden", 98 | data: { importLevel, fileLevel }, 99 | }) 100 | } 101 | }, 102 | } 103 | }, 104 | } 105 | -------------------------------------------------------------------------------- /packages/eslint-plugin/docs/rules/whitelist-levels-imports.md: -------------------------------------------------------------------------------- 1 | # @bem-react/whitelist-levels-imports 2 | 3 | This rule is needed to check imports according to redefinition levels whitelist. 4 | 5 | ## Options 6 | 7 | This rule has three options. 8 | 9 | ### `whiteList` (required) 10 | 11 | For example, in a project with three redefinition levels — `common`, `desktop` and `mobile`, you want to allow imports: 12 | 13 | * at `common` level only from `common` level 14 | * at `desktop` level from `common` and `desktop` levels 15 | * at `mobile` level from `common` and `mobile` levels 16 | 17 | Also you want to disallow imports: 18 | 19 | * at `common` level from `desktop` and `mobile` levels 20 | * at `desktop` level from `mobile` level 21 | * at `mobile` level from `desktop` level 22 | 23 | You can describe similar restrictions using `whiteList` option: 24 | 25 | ```json 26 | "whiteList": { 27 | "common": ["common"], 28 | "desktop": ["common", "desktop"], 29 | "mobile": ["common", "mobile"] 30 | } 31 | ``` 32 | 33 | Now at `common` redefinition level you can import only from `common` level. 34 | 35 | ```js 36 | // Button@common.tsx 37 | 38 | import { Icon } from '../Icon/Icon'; // Good 39 | import { Icon } from '../Icon/Icon@common'; // Good 40 | import { Icon } from '../Icon/Icon@mobile'; // No, it's wrong! 41 | import { Icon } from '../Icon/Icon@desktop'; // No, it's wrong! 42 | ``` 43 | 44 | At `desktop` redefinition level you can import from `common` and `desktop` levels, but not from `mobile` level. 45 | 46 | ```js 47 | // Button@desktop.tsx 48 | 49 | import { Icon } from '../Icon/Icon'; // Good 50 | import { Icon } from '../Icon/Icon@common'; // Good 51 | import { Icon } from '../Icon/Icon@desktop'; // Good 52 | import { Icon } from '../Icon/Icon@mobile'; // No, it's wrong! 53 | ``` 54 | 55 | And at `mobile` redefinition level you can import from `common` and `mobile` levels, but not from `desktop` level. 56 | 57 | ```js 58 | // Button@mobile.tsx 59 | 60 | import { Icon } from '../Icon/Icon'; // Good 61 | import { Icon } from '../Icon/Icon@common'; // Good 62 | import { Icon } from '../Icon/Icon@mobile'; // Good 63 | import { Icon } from '../Icon/Icon@desktop'; // No, it's wrong! 64 | ``` 65 | 66 | ### `defaultLevel` (required) 67 | 68 | The linter tries to determine a redefinition level of a file by it's filename. So for files like `Icon@desktop.tsx` and `Icon@desktop.example.tsx`, the redefinition level is `desktop`. For a file `mobile.tsx` the level is `mobile`, but only if this level is specified in the `whiteList` setting. 69 | 70 | If the linter could not determine a redefinition level by a filename, it accepts that level is a value of `defaultLevel` setting. In most project this setting has a `common` value, but you can use other, like `base` or like that. 71 | 72 | ### `ignorePaths` (optional) 73 | 74 | Additionally you can ignore some files, use `ignorePaths` option. For example: 75 | 76 | ```json 77 | "ignorePath": [ 78 | ".example.tsx$", 79 | ".example.jsx$" 80 | ] 81 | ``` 82 | 83 | ## Further Reading 84 | 85 | * [Redefinition levels](https://en.bem.info/methodology/redefinition-levels/) 86 | * [Dependency Injection](https://en.bem.info/technologies/bem-react/di/) 87 | -------------------------------------------------------------------------------- /packages/eslint-plugin/lib/rules/no-classname-runtime.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | meta: { 5 | docs: { 6 | description: 'Do not use the @bem-react/classname function in runtime code', 7 | category: 'perf', 8 | recommended: true, 9 | }, 10 | }, 11 | 12 | create: function(context) { 13 | function canOptimizeAllArguments(args) { 14 | // eslint-disable-next-line no-use-before-define 15 | return [].concat(args).every(canOptimize) 16 | } 17 | 18 | function isOptimizableCallExpression(node) { 19 | return node.type === 'CallExpression' && canOptimizeAllArguments(node.arguments) 20 | } 21 | 22 | function canOptimize(arg) { 23 | if (!arg) return false 24 | 25 | switch (arg.type) { 26 | case 'Literal': 27 | case 'Identifier': 28 | return true 29 | case 'MemberExpression': 30 | // Exception for this.props.x or this.state.x 31 | if ( 32 | arg.object.type === 'ThisExpression' 33 | && (arg.property.name === 'props' || arg.property.name === 'state') 34 | ) { 35 | return false 36 | } 37 | 38 | // Exception for props.x or state.x (case for destructuring) 39 | if ( 40 | arg.object.type === 'Identifier' 41 | && (arg.object.name === 'props' || arg.object.name === 'state') 42 | ) { 43 | return false 44 | } 45 | 46 | return canOptimize(arg.object) && canOptimize(arg.property) 47 | case 'Property': 48 | return canOptimize(arg.key) && canOptimize(arg.value) 49 | case 'ObjectExpression': 50 | return canOptimizeAllArguments(arg.properties) 51 | case 'ArrayExpression': 52 | return canOptimizeAllArguments(arg.elements) 53 | case 'ConditionalExpression': 54 | return canOptimizeAllArguments([arg.consequent, arg.alternate]) 55 | case 'BinaryExpression': 56 | case 'LogicalExpression': 57 | return canOptimizeAllArguments([arg.left, arg.right]) 58 | default: 59 | return false 60 | } 61 | } 62 | 63 | return { 64 | JSXAttribute: function(node) { 65 | const isEvaluatedClassName 66 | = node.name.type === 'JSXIdentifier' 67 | && node.name.name === 'className' 68 | && node.value.type === 'JSXExpressionContainer' 69 | 70 | if (!isEvaluatedClassName) return 71 | 72 | const expression = node.value.expression 73 | if ( 74 | // className={cn('x')} 75 | isOptimizableCallExpression(expression) 76 | // className={isA ? cn('A') : cn('B')} 77 | || (expression.type === 'ConditionalExpression' 78 | && (isOptimizableCallExpression(expression.consequent) 79 | || isOptimizableCallExpression(expression.alternate))) 80 | // className={isA && cn('A') || cn('B')} || className={isA ? cn('A') : cn('B')} || 81 | || ((expression.type === 'BinaryExpression' || expression.type === 'LogicalExpression') 82 | && (isOptimizableCallExpression(expression.left) 83 | || isOptimizableCallExpression(expression.right))) 84 | ) { 85 | context.report({ 86 | node, 87 | message: "You can speed up your code if you don't call the function on every render", 88 | }) 89 | } 90 | }, 91 | } 92 | }, 93 | } 94 | -------------------------------------------------------------------------------- /packages/eslint-plugin/tests/lib/rules/whitelist-levels-imports.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rule = require('../../../lib/rules/whitelist-levels-imports') 4 | const { RuleTester } = require('eslint') 5 | 6 | RuleTester.setDefaultConfig({ 7 | parserOptions: { 8 | sourceType: 'module', 9 | ecmaVersion: 2015, 10 | }, 11 | }) 12 | 13 | const ruleTester = new RuleTester() 14 | 15 | const filename = '/home/user/components/Link/Link@desktop.tsx' 16 | 17 | const options = [ 18 | { 19 | defaultLevel: 'common', 20 | whiteList: { 21 | common: ['common'], 22 | desktop: ['common', 'desktop'], 23 | mobile: ['common', 'mobile'], 24 | }, 25 | }, 26 | ] 27 | 28 | ruleTester.run('whitelist-levels-imports', rule, { 29 | valid: [ 30 | 'import "./styles.scss"', 31 | 'import "./styles@common.scss"', 32 | 'import "./styles@desktop.scss"', 33 | 'import * as keyset from "../i18n"', 34 | 'import { ILinkProps } from "."', 35 | 'import { ILinkProps } from ".."', 36 | 'import React from "react"', 37 | 'import Link from "@int/components"', 38 | 'import { Link } from "./Link"', 39 | 'import { Link } from "./Link@common"', 40 | 'import { Link } from "./Link@desktop"', 41 | 'import { Link } from "./Link@desktop.examples"', 42 | 'import { registry } from "./registry/desktop"', 43 | // There is no way to determine that 'unknown' is a redefinition level 44 | 'import { registry } from "./registry/unknown"', 45 | ].map((code) => ({ 46 | code, 47 | options, 48 | filename, 49 | })), 50 | 51 | invalid: [ 52 | { 53 | code: 'import "./styles@mobile.scss"', 54 | errors: [ 55 | { 56 | message: "Imports from 'mobile' level in files from 'desktop' level are forbidden", 57 | type: 'ImportDeclaration', 58 | }, 59 | ], 60 | }, 61 | { 62 | code: 'import "./styles@unknown.scss"', 63 | errors: [ 64 | { 65 | message: "Imports from 'unknown' level in files from 'desktop' level are forbidden", 66 | type: 'ImportDeclaration', 67 | }, 68 | ], 69 | }, 70 | { 71 | code: 'import { registry } from "./registry/mobile"', 72 | errors: [ 73 | { 74 | message: "Imports from 'mobile' level in files from 'desktop' level are forbidden", 75 | type: 'ImportDeclaration', 76 | }, 77 | ], 78 | }, 79 | { 80 | code: 'import { Link } from "./Link@mobile"', 81 | errors: [ 82 | { 83 | message: "Imports from 'mobile' level in files from 'desktop' level are forbidden", 84 | type: 'ImportDeclaration', 85 | }, 86 | ], 87 | }, 88 | { 89 | code: 'import { Link } from "./Link@unknown"', 90 | errors: [ 91 | { 92 | message: "Imports from 'unknown' level in files from 'desktop' level are forbidden", 93 | type: 'ImportDeclaration', 94 | }, 95 | ], 96 | }, 97 | { 98 | code: 'import { Link } from "./Link@mobile.examples"', 99 | errors: [ 100 | { 101 | message: "Imports from 'mobile' level in files from 'desktop' level are forbidden", 102 | type: 'ImportDeclaration', 103 | }, 104 | ], 105 | }, 106 | ].map((item) => ({ 107 | ...item, 108 | options, 109 | filename, 110 | })), 111 | }) 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bem-react · [![github (ci)](https://github.com/bem/bem-react/workflows/ci/badge.svg?branch=master)](https://github.com/bem/bem-react/workflows/ci/badge.svg?branch=master) 2 | 3 | A set of tools for developing user interfaces using the [BEM methodology](https://en.bem.info) in [React](https://github.com/facebook/react). BEM React supports [TypeScript](https://www.typescriptlang.org/index.html) type annotations. 4 | 5 | ## Packages 6 | 7 | | Package | Version | Size | 8 | | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 9 | | [`@bem-react/classname`](packages/classname) | [![npm (scoped)](https://img.shields.io/npm/v/@bem-react/classname.svg)](https://www.npmjs.com/package/@bem-react/classname) | [![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/@bem-react/classname.svg)](https://bundlephobia.com/result?p=@bem-react/classname) | 10 | | [`@bem-react/classnames`](packages/classnames) | [![npm (scoped)](https://img.shields.io/npm/v/@bem-react/classnames.svg)](https://www.npmjs.com/package/@bem-react/classnames) | [![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/@bem-react/classnames.svg)](https://bundlephobia.com/result?p=@bem-react/classnames) | 11 | | [`@bem-react/core`](packages/core) | [![npm (scoped)](https://img.shields.io/npm/v/@bem-react/core.svg)](https://www.npmjs.com/package/@bem-react/core) | [![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/@bem-react/core.svg)](https://bundlephobia.com/result?p=@bem-react/core) | 12 | | [`@bem-react/di`](packages/di) | [![npm (scoped)](https://img.shields.io/npm/v/@bem-react/di.svg)](https://www.npmjs.com/package/@bem-react/di) | [![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/@bem-react/di.svg)](https://bundlephobia.com/result?p=@bem-react/di) | 13 | | [`@bem-react/eslint-plugin`](packages/eslint-plugin) | [![npm (scoped)](https://img.shields.io/npm/v/@bem-react/eslint-plugin.svg)](https://www.npmjs.com/package/@bem-react/eslint-plugin) | — | 14 | 15 | ## Contribute 16 | 17 | Bem React Core is an open source library that is under active development and is also used within [Yandex](https://yandex.com/company/). 18 | 19 | If you have suggestions for improving the API, you can send us a [Pull Request](https://github.com/bem/bem-react-core/pulls). 20 | 21 | If you found a bug, you can create an [issue](https://github.com/bem/bem-react-core/issues) describing the problem. 22 | 23 | For a detailed guide to making suggestions, see: [CONTRIBUTING.md](CONTRIBUTING.md). 24 | 25 | ## License 26 | 27 | © 2018 [Yandex](https://yandex.com/company/). Code released under [Mozilla Public License 2.0](LICENSE.md). 28 | -------------------------------------------------------------------------------- /packages/classnames/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [1.4.0](https://github.com/bem/bem-react/compare/@bem-react/classnames@1.3.10...@bem-react/classnames@1.4.0) (2025-06-15) 7 | 8 | ### Features 9 | 10 | - add dual packaging ([6d78a65](https://github.com/bem/bem-react/commit/6d78a651214abb057390338e4ea249c264c3252d)) 11 | - support esm modules ([7ef51f8](https://github.com/bem/bem-react/commit/7ef51f8a96d1b81f60458a4772237813eb630c42)) 12 | 13 | ## [1.3.10](https://github.com/bem/bem-react/compare/@bem-react/classnames@1.3.9...@bem-react/classnames@1.3.10) (2021-06-08) 14 | 15 | ### Bug Fixes 16 | 17 | - update pkg ([1ccdee8](https://github.com/bem/bem-react/commit/1ccdee8d9c4c09a02f888ee880a332ac75b725fd)) 18 | 19 | ## [1.3.9](https://github.com/bem/bem-react/compare/@bem-react/classnames@1.3.8...@bem-react/classnames@1.3.9) (2020-03-12) 20 | 21 | **Note:** Version bump only for package @bem-react/classnames 22 | 23 | ## [1.3.8](https://github.com/bem/bem-react/compare/@bem-react/classnames@1.3.7...@bem-react/classnames@1.3.8) (2020-03-02) 24 | 25 | **Note:** Version bump only for package @bem-react/classnames 26 | 27 | ## [1.3.7](https://github.com/bem/bem-react/compare/@bem-react/classnames@1.3.6...@bem-react/classnames@1.3.7) (2019-10-02) 28 | 29 | **Note:** Version bump only for package @bem-react/classnames 30 | 31 | ## [1.3.6](https://github.com/bem/bem-react/compare/@bem-react/classnames@1.3.5...@bem-react/classnames@1.3.6) (2019-10-02) 32 | 33 | **Note:** Version bump only for package @bem-react/classnames 34 | 35 | ## [1.3.5](https://github.com/bem/bem-react/compare/@bem-react/classnames@1.3.4...@bem-react/classnames@1.3.5) (2019-08-20) 36 | 37 | **Note:** Version bump only for package @bem-react/classnames 38 | 39 | ## [1.3.4](https://github.com/bem/bem-react/compare/@bem-react/classnames@1.3.3...@bem-react/classnames@1.3.4) (2019-05-27) 40 | 41 | **Note:** Version bump only for package @bem-react/classnames 42 | 43 | ## [1.3.3](https://github.com/bem/bem-react/tree/master/packages/classnames/compare/@bem-react/classnames@1.3.2...@bem-react/classnames@1.3.3) (2019-05-24) 44 | 45 | **Note:** Version bump only for package @bem-react/classnames 46 | 47 | ## [1.3.2](https://github.com/bem/bem-react/tree/master/packages/classnames/compare/@bem-react/classnames@1.3.1...@bem-react/classnames@1.3.2) (2019-04-22) 48 | 49 | **Note:** Version bump only for package @bem-react/classnames 50 | 51 | ## [1.3.1](https://github.com/bem/bem-react/tree/master/packages/classnames/compare/@bem-react/classnames@1.3.0...@bem-react/classnames@1.3.1) (2018-12-28) 52 | 53 | **Note:** Version bump only for package @bem-react/classnames 54 | 55 | # 1.3.0 (2018-12-21) 56 | 57 | ### Features 58 | 59 | - **classname:** decrease bundle size, classnames pkg ([c5fb74f](https://github.com/bem/bem-react/commit/c5fb74f)) 60 | 61 | ## 1.2.2 (2018-12-21) 62 | 63 | ### Features 64 | 65 | - **classname:** decrease bundle size, classnames pkg ([c5fb74f](https://github.com/bem/bem-react/commit/c5fb74f)) 66 | 67 | ## 1.2.1 (2018-12-19) 68 | 69 | ### Features 70 | 71 | - **classname:** decrease bundle size, classnames pkg ([c5fb74f](https://github.com/bem/bem-react/commit/c5fb74f)) 72 | 73 | # 1.2.0 (2018-12-18) 74 | 75 | ### Features 76 | 77 | - **classname:** decrease bundle size, classnames pkg ([c5fb74f](https://github.com/bem/bem-react/commit/c5fb74f)) 78 | 79 | # 1.1.0 (2018-12-06) 80 | 81 | ### Features 82 | 83 | - **classname:** decrease bundle size, classnames pkg ([c5fb74f](https://github.com/bem/bem-react/commit/c5fb74f)) 84 | -------------------------------------------------------------------------------- /packages/core/test/compose.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, ComponentType, ReactNode } from 'react' 2 | import { render } from '@testing-library/react' 3 | 4 | import { compose, composeU, createClassNameModifier, withBemMod } from '../core' 5 | 6 | type BaseProps = { 7 | text: string 8 | children?: ReactNode 9 | } 10 | 11 | type HoveredProps = { 12 | hovered?: boolean 13 | } 14 | 15 | type ThemeAProps = { 16 | theme?: 'a' 17 | } 18 | 19 | type ThemeBProps = { 20 | theme?: 'b' 21 | } 22 | 23 | type SizeAProps = { 24 | size?: 'a' 25 | } 26 | 27 | type SimpleProps = { 28 | simple?: 'somevalue' | 'errorvalue' 29 | } 30 | 31 | type SimpleBooleanProps = { 32 | isBoolean?: boolean 33 | } 34 | 35 | function expectAttrs(Component: React.ReactElement, attrs: { [key: string]: string }) { 36 | const { container } = render(Component) 37 | const node = container.querySelector('*') 38 | for (const a in attrs) { 39 | expect(node?.getAttribute(a)).toEqual(attrs[a]) 40 | } 41 | } 42 | 43 | const Component: FC = ({ children }) =>
{children}
44 | const ComponentSpreadProps: FC = ({ children, ...props }) => { 45 | if ( 46 | // @ts-ignore 47 | (props.hasOwnProperty('isBoolean') && props.isBoolean === undefined) 48 | // @ts-ignore 49 | || props.isBoolean === false 50 | ) { 51 | throw Error('props must be omitted') 52 | } 53 | 54 | return
{children}
55 | } 56 | const withSimpleCompose = createClassNameModifier('EnhancedComponent', { 57 | simple: 'somevalue', 58 | }) 59 | 60 | const withSimpleBooleanCompose = createClassNameModifier('EnhancedComponent', { 61 | isBoolean: true, 62 | }) 63 | 64 | const withAutoSimpleCompose = withBemMod<{ autosimple?: 'yes' }>('EnhancedComponent', { 65 | autosimple: 'yes', 66 | }) 67 | 68 | const withHover = (Wrapped: ComponentType) => (props: HoveredProps) => 69 | const withThemeA = (Wrapped: ComponentType) => (props: ThemeAProps) => 70 | const withThemeB = (Wrapped: ComponentType) => (props: ThemeBProps) => 71 | const withSizeA = (Wrapped: ComponentType) => (props: SizeAProps) => 72 | 73 | const EnhancedComponent = compose( 74 | withSimpleCompose, 75 | withAutoSimpleCompose, 76 | withHover, 77 | withSizeA, 78 | composeU(withThemeA, withThemeB), 79 | )(Component) 80 | 81 | const EnhancedComponentRemoveProp = compose( 82 | withSimpleCompose, 83 | withSimpleBooleanCompose, 84 | withAutoSimpleCompose, 85 | withHover, 86 | withThemeB, 87 | )(ComponentSpreadProps) 88 | 89 | describe('compose', () => { 90 | test('should compile component with theme a', () => { 91 | render() 92 | }) 93 | 94 | test('should compile component with theme b', () => { 95 | render() 96 | }) 97 | 98 | test('should compile component with hovered true', () => { 99 | render() 100 | }) 101 | 102 | test('should compile component with simple mod', () => { 103 | render() 104 | }) 105 | 106 | test('remove mod props in simple mod', () => { 107 | expectAttrs( 108 | , 115 | { 116 | autosimple: 'yes', 117 | class: 118 | 'EnhancedComponent EnhancedComponent_simple_somevalue EnhancedComponent_isBoolean EnhancedComponent_autosimple_yes', 119 | text: '', 120 | theme: 'b', 121 | }, 122 | ) 123 | }) 124 | 125 | test('remove mod props in simple mod if boolean value', () => { 126 | expectAttrs(, { 127 | class: 'EnhancedComponent', 128 | text: '', 129 | }) 130 | }) 131 | 132 | test("don't remove mod props in simple mod if value hasn't matched", () => { 133 | expectAttrs( 134 | , 135 | { 136 | class: 'EnhancedComponent', 137 | simple: 'errorvalue', 138 | text: '', 139 | theme: 'b', 140 | }, 141 | ) 142 | }) 143 | }) 144 | -------------------------------------------------------------------------------- /packages/classname/classname.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * List of classname. 3 | */ 4 | export type ClassNameList = string | Array 5 | 6 | /** 7 | * Allowed modifiers format. 8 | * 9 | * @see https://en.bem.info/methodology/key-concepts/#modifier 10 | */ 11 | export type NoStrictEntityMods = Record 12 | 13 | /** 14 | * BEM Entity className initializer. 15 | */ 16 | export type ClassNameInitilizer = (blockName: string, elemName?: string) => ClassNameFormatter 17 | 18 | /** 19 | * BEM Entity className formatter. 20 | */ 21 | export interface ClassNameFormatter { 22 | (): string 23 | (mods?: NoStrictEntityMods | null, mix?: ClassNameList): string 24 | (elemName: string, elemMix?: ClassNameList): string 25 | (elemName: string, elemMods?: NoStrictEntityMods | null, elemMix?: ClassNameList): string 26 | } 27 | 28 | /** 29 | * Settings for the naming convention. 30 | */ 31 | export type Preset = { 32 | /** 33 | * Global namespace. 34 | * 35 | * @example `omg-Bem-Elem_mod_val` 36 | */ 37 | n?: string 38 | /** 39 | * Elem's delimiter. 40 | */ 41 | e?: string 42 | /** 43 | * Modifier's delimiter. 44 | */ 45 | m?: string 46 | /** 47 | * Modifier value delimiter. 48 | */ 49 | v?: string 50 | } 51 | 52 | /** 53 | * BEM className configure function. 54 | * 55 | * @example 56 | * ``` ts 57 | * 58 | * import { withNaming } from '@bem-react/classname'; 59 | * 60 | * const cn = withNaming({ n: 'ns-', e: '__', m: '_' }); 61 | * 62 | * cn('block', 'elem'); // 'ns-block__elem' 63 | * ``` 64 | * 65 | * @param preset settings for the naming convention 66 | */ 67 | export function withNaming(preset: Preset): ClassNameInitilizer { 68 | const nameSpace = preset.n || '' 69 | const modValueDelimiter = preset.v || preset.m 70 | 71 | function stringify(b: string, e?: string, m?: NoStrictEntityMods | null, mix?: ClassNameList) { 72 | const entityName = e ? nameSpace + b + preset.e + e : nameSpace + b 73 | let className = entityName 74 | 75 | if (m) { 76 | const modPrefix = ' ' + className + preset.m 77 | 78 | for (const k in m) { 79 | if (m.hasOwnProperty(k)) { 80 | const modVal = m[k] 81 | 82 | if (modVal === true) { 83 | className += modPrefix + k 84 | } else if (modVal) { 85 | className += modPrefix + k + modValueDelimiter + modVal 86 | } 87 | } 88 | } 89 | } 90 | 91 | if (mix !== undefined) { 92 | mix = Array.isArray(mix) ? mix : [mix] 93 | 94 | for (let i = 0, len = mix.length; i < len; i++) { 95 | const value = mix[i] 96 | 97 | // Skipping non-string values and empty strings 98 | if (!value || typeof value.valueOf() !== 'string') continue 99 | 100 | const mixes = value.valueOf().split(' ') 101 | 102 | for (let j = 0; j < mixes.length; j++) { 103 | const val = mixes[j] 104 | if (val !== entityName) { 105 | className += ' ' + val 106 | } 107 | } 108 | } 109 | } 110 | 111 | return className 112 | } 113 | 114 | return function cnGenerator(b: string, e?: string): ClassNameFormatter { 115 | return ( 116 | elemOrMods?: NoStrictEntityMods | string | null, 117 | elemModsOrBlockMix?: NoStrictEntityMods | ClassNameList | null, 118 | elemMix?: ClassNameList, 119 | ) => { 120 | if (typeof elemOrMods === 'string') { 121 | if (typeof elemModsOrBlockMix === 'string' || Array.isArray(elemModsOrBlockMix)) { 122 | return stringify(b, elemOrMods, undefined, elemModsOrBlockMix) 123 | } 124 | return stringify(b, elemOrMods, elemModsOrBlockMix, elemMix) 125 | } 126 | return stringify(b, e, elemOrMods, elemModsOrBlockMix as ClassNameList) 127 | } 128 | } 129 | } 130 | 131 | /** 132 | * BEM Entity className initializer with React naming preset. 133 | * 134 | * @example 135 | * ``` ts 136 | * 137 | * import { cn } from '@bem-react/classname'; 138 | * 139 | * const cat = cn('Cat'); 140 | * 141 | * cat(); // Cat 142 | * cat({ size: 'm' }); // Cat_size_m 143 | * cat('Tail'); // Cat-Tail 144 | * cat('Tail', { length: 'small' }); // Cat-Tail_length_small 145 | * 146 | * const dogPaw = cn('Dog', 'Paw'); 147 | * 148 | * dogPaw(); // Dog-Paw 149 | * dogPaw({ color: 'black', exists: true }); // Dog-Paw_color_black Dog-Paw_exists 150 | * ``` 151 | * 152 | * @see https://en.bem.info/methodology/naming-convention/#react-style 153 | */ 154 | export const cn = withNaming({ 155 | e: '-', 156 | m: '_', 157 | }) 158 | -------------------------------------------------------------------------------- /scripts/rollup/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { resolve } = require('path') 4 | const { readFileSync, existsSync } = require('fs') 5 | const { parseConfigFileTextToJson } = require('typescript') 6 | const rimraf = require('rimraf') 7 | const gzipSize = require('gzip-size') 8 | const prettyBytes = require('pretty-bytes') 9 | const { rollup } = require('rollup') 10 | const typescript2 = require('rollup-plugin-typescript2') 11 | const replace = require('rollup-plugin-replace') 12 | const nodeResolve = require('rollup-plugin-node-resolve') 13 | const stripBanner = require('rollup-plugin-strip-banner') 14 | 15 | const { log } = console 16 | const packagePath = process.cwd() 17 | 18 | function getPlugins({ isProduction, tsConfigPath }) { 19 | return [ 20 | stripBanner(), 21 | nodeResolve(), 22 | replace({ __DEV__: !isProduction }), 23 | typescript2({ 24 | clean: true, 25 | tsconfig: tsConfigPath, 26 | useTsconfigDeclarationDir: true, 27 | }), 28 | ] 29 | } 30 | 31 | function getExternalDependencies(packagePath) { 32 | const packageJsonPath = resolve(packagePath, 'package.json') 33 | const content = readFileSync(packageJsonPath, 'utf-8') 34 | const { dependencies, peerDependencies } = JSON.parse(content) 35 | 36 | return [...Object.keys(Object(dependencies)), ...Object.keys(Object(peerDependencies))] 37 | } 38 | 39 | function getTypescriptConfig(packagePath) { 40 | const tsConfigPath = resolve(packagePath, 'tsconfig.json') 41 | const content = readFileSync(tsConfigPath, 'utf-8') 42 | const { config } = parseConfigFileTextToJson(tsConfigPath, content) 43 | 44 | return { tsConfigPath, tsConfig: config } 45 | } 46 | 47 | function getInputFilePath(packagePath, packageName) { 48 | let filePath = resolve(packagePath, `${packageName}.ts`) 49 | // Try check file with `ts` extension. 50 | if (existsSync(filePath)) { 51 | return filePath 52 | } 53 | 54 | filePath = filePath.replace('.ts', '.tsx') 55 | // Try check file with `tsx` extension. 56 | if (existsSync(filePath)) { 57 | return filePath 58 | } 59 | 60 | throw new Error(`Cannot found main file for package "${packageName}".`) 61 | } 62 | 63 | function getPackageData(packagePath) { 64 | const externalDependencies = getExternalDependencies(packagePath) 65 | const { tsConfigPath, tsConfig } = getTypescriptConfig(packagePath) 66 | const packageName = packagePath.split('/').pop() 67 | const buildPath = resolve(packagePath, tsConfig.compilerOptions.outDir) 68 | const inputFile = getInputFilePath(packagePath, packageName) 69 | 70 | return { 71 | externalDependencies, 72 | packageName, 73 | tsConfigPath, 74 | inputFile, 75 | outputs: [ 76 | { 77 | outputFile: resolve(buildPath, `${packageName}.cjs`), 78 | isProduction: true, 79 | isESM: false, 80 | }, 81 | { 82 | outputFile: resolve(buildPath, `${packageName}.mjs`), 83 | isProduction: true, 84 | isESM: true, 85 | }, 86 | ], 87 | } 88 | } 89 | 90 | function build({ packageName, tsConfigPath, externalDependencies, inputFile, outputs }) { 91 | outputs.forEach(async ({ outputFile, isProduction, isESM }) => { 92 | const inputConfig = { 93 | input: inputFile, 94 | plugins: getPlugins({ isProduction, tsConfigPath }), 95 | external: externalDependencies, 96 | } 97 | 98 | const outputConfig = { 99 | file: outputFile, 100 | format: isESM ? 'es' : 'cjs', 101 | interop: false, 102 | } 103 | 104 | try { 105 | log(`❯ Building(📦): ${packageName} (${isProduction ? 'production' : 'development'})`) 106 | const hrstart = process.hrtime() 107 | const result = await rollup(inputConfig) 108 | const writer = await result.write(outputConfig) 109 | const hrend = process.hrtime(hrstart) 110 | const executionTime = parseInt((hrend[0] * 1e9 + hrend[1]) / 1e6, 10) 111 | const bundleGzipSize = prettyBytes(gzipSize.sync(writer.output[0].code)) 112 | log(`❯ Complete(🤘): ${outputFile} (${executionTime}ms) [gzip: ${bundleGzipSize}]`) 113 | } catch (error) { 114 | log(`❯ Building(💥): ${error}`) 115 | } 116 | }) 117 | } 118 | 119 | function cleanup(packagePath) { 120 | log(`❯ Cleanup: ${packagePath}`) 121 | const { tsConfig } = getTypescriptConfig(packagePath) 122 | try { 123 | rimraf.sync(resolve(packagePath, tsConfig.compilerOptions.outDir)) 124 | rimraf.sync(resolve(packagePath, tsConfig.compilerOptions.declarationDir, '*.d.ts')) 125 | } catch (error) { 126 | log(`❯ Cleanup(💥): ${error}`) 127 | } 128 | } 129 | 130 | cleanup(packagePath) 131 | build(getPackageData(packagePath)) 132 | -------------------------------------------------------------------------------- /packages/core/test/withBemMod.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { render } from '@testing-library/react' 3 | import { cn } from '@bem-react/classname' 4 | 5 | import { withBemMod, IClassNameProps } from '../core' 6 | 7 | const getClassNameFromSelector = (Component: React.ReactElement, selector: string = 'div') => { 8 | const { container } = render(Component) 9 | return container.querySelector(selector)?.className 10 | } 11 | 12 | interface IPresenterProps extends IClassNameProps { 13 | theme?: 'normal' 14 | view?: 'default' 15 | url?: string 16 | } 17 | 18 | const presenter = cn('Presenter') 19 | 20 | const Presenter: React.FC = ({ className }) => ( 21 |
22 | ) 23 | 24 | describe('withBemMod', () => { 25 | test('should not affect CSS class with empty object', () => { 26 | const WBCM = withBemMod(presenter(), {})(Presenter) 27 | expect(getClassNameFromSelector()).toEqual( 28 | 'Presenter Additional', 29 | ) 30 | }) 31 | 32 | test('should add modifier class for matched prop', () => { 33 | const Enhanced1 = withBemMod(presenter(), { theme: 'normal' })(Presenter) 34 | const Enhanced2 = withBemMod(presenter(), { view: 'default' })(Enhanced1) 35 | const Component = 36 | 37 | expect(getClassNameFromSelector(Component)).toEqual( 38 | 'Presenter Presenter_theme_normal Presenter_view_default Additional', 39 | ) 40 | }) 41 | 42 | test('should not add modifier class for star matched prop', () => { 43 | const Enhanced1 = withBemMod(presenter(), { url: '*' })(Presenter) 44 | const Component = 45 | 46 | expect(getClassNameFromSelector(Component)).toEqual('Presenter Additional') 47 | }) 48 | 49 | test('should match on star matched prop', () => { 50 | const Enhanced1 = withBemMod(presenter(), { url: '*' }, (Base) => (props) => ( 51 | 52 | ))(Presenter) 53 | const Component = 54 | 55 | expect(getClassNameFromSelector(Component)).toEqual('Presenter Additional') 56 | }) 57 | 58 | test('should not add modifier class for unmatched prop', () => { 59 | const WBCM = withBemMod(presenter(), { theme: 'normal' })(Presenter) 60 | expect(getClassNameFromSelector()).toEqual( 61 | 'Presenter Additional', 62 | ) 63 | }) 64 | 65 | test('should not initialized after change props', () => { 66 | const init = jest.fn() 67 | const Enhanced = withBemMod( 68 | presenter(), 69 | { theme: 'normal' }, 70 | (WrappedComponent) => 71 | class WithEnhanced extends React.PureComponent { 72 | constructor(props: IPresenterProps) { 73 | super(props) 74 | init() 75 | } 76 | 77 | render() { 78 | return 79 | } 80 | }, 81 | )(Presenter) 82 | 83 | const { container } = render() 84 | render(, { container }) 85 | expect(init).toHaveBeenCalledTimes(1) 86 | }) 87 | 88 | test('should cache new component for every new call of `withBemMod` returned function', () => { 89 | const withTheme = withBemMod( 90 | presenter(), 91 | { theme: 'normal' }, 92 | (Base) => (props) => , 93 | ) 94 | const withView = withBemMod( 95 | presenter(), 96 | { view: 'default' }, 97 | (Base) => (props) => , 98 | ) 99 | 100 | const Enhanced1 = withTheme(Presenter) 101 | const Enhanced2 = withTheme(withView(Presenter)) 102 | 103 | render() 104 | expect(getClassNameFromSelector()).toEqual( 105 | 'Presenter Presenter_view_default Presenter_theme_normal', 106 | ) 107 | }) 108 | 109 | test('should forward ref', () => { 110 | class PresenterClass extends Component { 111 | render() { 112 | return
113 | } 114 | } 115 | 116 | const withTheme = withBemMod(presenter(), { theme: 'normal' }) 117 | 118 | // Unfortunately, manual type cast is necessary 119 | const Enhanced = withTheme(PresenterClass) as any as React.ForwardRefExoticComponent< 120 | IPresenterProps & { ref: React.Ref } 121 | > 122 | 123 | const ref = React.createRef() 124 | 125 | render() 126 | expect(ref.current).toBeInstanceOf(PresenterClass) 127 | }) 128 | }) 129 | -------------------------------------------------------------------------------- /packages/pack/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [3.1.0](https://github.com/bem/bem-react/compare/@bem-react/pack@3.0.0...@bem-react/pack@3.1.0) (2023-06-01) 7 | 8 | 9 | ### Features 10 | 11 | * impl ts resolve alias plugin ([2fc5c2f](https://github.com/bem/bem-react/commit/2fc5c2f64108cbc558d713a615f4425af660d4fd)) 12 | 13 | 14 | 15 | 16 | 17 | # [3.0.0](https://github.com/bem/bem-react/compare/@bem-react/pack@1.6.0...@bem-react/pack@3.0.0) (2022-12-15) 18 | 19 | **Note:** Version bump only for package @bem-react/pack 20 | 21 | # [2.0.0](https://github.com/bem/bem-react/compare/@bem-react/pack@1.6.0...@bem-react/pack@2.0.0) (2022-12-15) 22 | 23 | **Note:** Version bump only for package @bem-react/pack 24 | 25 | # [1.6.0](https://github.com/bem/bem-react/compare/@bem-react/pack@1.5.1...@bem-react/pack@1.6.0) (2021-09-03) 26 | 27 | ### Bug Fixes 28 | 29 | - **pack:** remove unused replace option ([8bd86c2](https://github.com/bem/bem-react/commit/8bd86c2a47d82daa93900060e88a696f24456c70)) 30 | 31 | ### Features 32 | 33 | - **pack:** call onAfterRun when process is interrupted ([2b87be8](https://github.com/bem/bem-react/commit/2b87be8384f17168f579c1bcc016a95e0ea208a5)) 34 | - **pack:** impl BabelTypeScriptPlugin plugin ([6e1adee](https://github.com/bem/bem-react/commit/6e1adee89b6056c1739965e38a8688e91d606b86)) 35 | 36 | ## [1.5.1](https://github.com/bem/bem-react/compare/@bem-react/pack@1.5.0...@bem-react/pack@1.5.1) (2021-06-23) 37 | 38 | ### Bug Fixes 39 | 40 | - **pack:** update fast-glob from 3.2.4 to 3.2.5 ([46a70f1](https://github.com/bem/bem-react/commit/46a70f1f4ff50a61817853c5ada0228d5b214332)) 41 | 42 | # [1.5.0](https://github.com/bem/bem-react/compare/@bem-react/pack@1.4.1...@bem-react/pack@1.5.0) (2021-06-22) 43 | 44 | ### Features 45 | 46 | - **pack:** impl usePackageJsonPlugin plugin ([69789db](https://github.com/bem/bem-react/commit/69789db5f6e60e6ac9d4d7f69b3086ccb689d1fc)) 47 | - **pack:** remove replace option ([88a6338](https://github.com/bem/bem-react/commit/88a6338ff31ee124bd067f6a8de0528b8c6a8274)) 48 | 49 | ## [1.4.1](https://github.com/bem/bem-react/compare/@bem-react/pack@1.4.0...@bem-react/pack@1.4.1) (2021-02-10) 50 | 51 | ### Bug Fixes 52 | 53 | - move postcss to dev-deps ([335b9b1](https://github.com/bem/bem-react/commit/335b9b1f4a2567427c51a61304d259643ad0b6fb)) 54 | 55 | # [1.4.0](https://github.com/bem/bem-react/compare/@bem-react/pack@1.3.0...@bem-react/pack@1.4.0) (2020-12-31) 56 | 57 | ### Bug Fixes 58 | 59 | - catch errors from wrapToPromise ([f417e0c](https://github.com/bem/bem-react/commit/f417e0cea2e1009a0367e6e3118b6dfe69654b5a)) 60 | 61 | ### Features 62 | 63 | - add silent mode for output ([60f57eb](https://github.com/bem/bem-react/commit/60f57eba7e5e74a6ac4dd779c79372c003ed7710)) 64 | - impl detailed log for building ([a9ce426](https://github.com/bem/bem-react/commit/a9ce426f8e866b817959813d3f7b15fb4b9f7ce7)) 65 | 66 | # [1.3.0](https://github.com/bem/bem-react/compare/@bem-react/pack@1.2.0...@bem-react/pack@1.3.0) (2020-11-24) 67 | 68 | ### Features 69 | 70 | - pack – multiple config output ([8956088](https://github.com/bem/bem-react/commit/8956088d59a7c71ce4dd54d2db65a2efc24962b4)) 71 | 72 | # [1.2.0](https://github.com/bem/bem-react/compare/@bem-react/pack@1.1.0...@bem-react/pack@1.2.0) (2020-11-11) 73 | 74 | ### Bug Fixes 75 | 76 | - add formatting for result package.json ([fa43688](https://github.com/bem/bem-react/commit/fa43688be7d66567cb9cb88f088786f3b264b033)) 77 | - allow boolean type for sideEffects ([a915778](https://github.com/bem/bem-react/commit/a915778957269667a4f049830949455facdb9782)) 78 | - use default value for options ([4c4c60f](https://github.com/bem/bem-react/commit/4c4c60f2e6d62be1ff450e67549385cc3f820b21)) 79 | - use js ext for resolve default config ([af53d13](https://github.com/bem/bem-react/commit/af53d13a3365659f1aa4db0e211672303053a51e)) 80 | 81 | ### Features 82 | 83 | - impl replace content for TypeScriptPlugin ([00b1670](https://github.com/bem/bem-react/commit/00b167092be6f786030984a1f29bcbdb7da1bb2f)) 84 | 85 | # [1.1.0](https://github.com/bem/bem-react/compare/@bem-react/pack@1.0.4...@bem-react/pack@1.1.0) (2020-10-23) 86 | 87 | ### Features 88 | 89 | - add hook for create sideEffects ([b510cfe](https://github.com/bem/bem-react/commit/b510cfe660ee6251cda86a493d462e5b2e775be2)) 90 | 91 | ## [1.0.4](https://github.com/bem/bem-react/compare/@bem-react/pack@1.0.3...@bem-react/pack@1.0.4) (2020-10-08) 92 | 93 | ### Bug Fixes 94 | 95 | - **pack:** support async postcss plugins ([7e1d45f](https://github.com/bem/bem-react/commit/7e1d45fddc825ab3e9e2cd10ae93e5761617fcc9)) 96 | 97 | ## [1.0.3](https://github.com/bem/bem-react/compare/@bem-react/pack@1.0.2...@bem-react/pack@1.0.3) (2020-09-04) 98 | 99 | ### Bug Fixes 100 | 101 | - extends sideEffects for platforms ([b80a734](https://github.com/bem/bem-react/commit/b80a734464031d0d9724d66d813cbc4decc22784)) 102 | 103 | ## [1.0.2](https://github.com/bem/bem-react/compare/@bem-react/pack@1.0.1...@bem-react/pack@1.0.2) (2020-08-28) 104 | 105 | ### Bug Fixes 106 | 107 | - use wildcard for css sideeffects ([8bb81f2](https://github.com/bem/bem-react/commit/8bb81f2fd70b16c7fd885578889a86ab0268320f)) 108 | 109 | ## [1.0.1](https://github.com/bem/bem-react/compare/@bem-react/pack@1.0.0...@bem-react/pack@1.0.1) (2020-08-28) 110 | 111 | ### Bug Fixes 112 | 113 | - add pkg with sideEffects for esm build ([a86c32b](https://github.com/bem/bem-react/commit/a86c32bd303ca9d043769077fe9650b523640760)) 114 | -------------------------------------------------------------------------------- /packages/pack/src/plugins/BabelTypeScriptPlugin.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process' 2 | import { promisify } from 'util' 3 | import { resolve, join, dirname, relative } from 'path' 4 | import { writeJson } from 'fs-extra' 5 | import glob from 'fast-glob' 6 | import ts from 'typescript' 7 | import { removeSync, writeJSONSync } from 'fs-extra' 8 | 9 | import { Plugin, OnDone, HookOptions } from '../interfaces' 10 | import { mark } from '../debug' 11 | 12 | const execAsync = promisify(exec) 13 | 14 | type Options = { 15 | /** 16 | * A path to typescript config. 17 | */ 18 | tsConfigPath?: string 19 | /** 20 | * A path to babel config. 21 | */ 22 | babelConfigPath?: string 23 | /** 24 | * A callback for when creating side effects. 25 | */ 26 | onCreateSideEffects: (path: string) => string[] | boolean | undefined 27 | } 28 | 29 | class BabelTypeScriptPlugin implements Plugin { 30 | name = 'BabelTypeScriptPlugin' 31 | 32 | private context = '' 33 | private output = '' 34 | private precompiledConfigPath = '' 35 | private precompiledDir = '' 36 | 37 | constructor(public options: Options = {} as Options) { 38 | mark('BabelTypeScriptPlugin::constructor') 39 | } 40 | 41 | async onStart(done: OnDone, { context, output }: HookOptions) { 42 | this.options.tsConfigPath = this.options.tsConfigPath || 'tsconfig.json' 43 | this.options.babelConfigPath = this.options.babelConfigPath || 'babel.config.js' 44 | 45 | this.precompiledDir = '__precompiled__' 46 | this.precompiledConfigPath = join(context, 'tsconfig.__precompiled__.json') 47 | this.context = context 48 | this.output = output 49 | done() 50 | } 51 | 52 | async onBeforeRun(done: OnDone) { 53 | this.generatePrecompiledTsConfig() 54 | done() 55 | } 56 | 57 | async onRun(done: OnDone) { 58 | mark('BabelTypeScriptPlugin::onRun(start)') 59 | 60 | try { 61 | await this.generateTypings() 62 | await this.compile() 63 | await this.generateModulePackage() 64 | } catch (e) { 65 | const error = e as any 66 | throw new Error(error.stdout || error.stderr) 67 | } 68 | 69 | mark('BabelTypeScriptPlugin::onRun(finish)') 70 | done() 71 | } 72 | 73 | async onAfterRun(done: OnDone) { 74 | removeSync(this.precompiledConfigPath) 75 | removeSync(this.precompiledDir) 76 | done() 77 | } 78 | 79 | private async compile() { 80 | const babelConfigPath = this.options.babelConfigPath 81 | const precompiledOutDir = this.precompiledDir 82 | const tsConfigPath = this.precompiledConfigPath 83 | const config = this.getTsConfig() 84 | const { rootDir, outDir } = config.options 85 | 86 | // prettier-ignore 87 | await execAsync(`npx babel ${rootDir} --config-file ${babelConfigPath} --out-dir ${precompiledOutDir} --extensions .ts,.tsx`) 88 | await Promise.all([ 89 | // prettier-ignore 90 | execAsync(`npx tsc -p ${tsConfigPath} --module commonjs --outDir ${outDir!}`), 91 | // prettier-ignore 92 | execAsync(`npx tsc -p ${tsConfigPath} --module esnext --outDir ${resolve(outDir!, 'esm')}`), 93 | ]) 94 | } 95 | 96 | private async generateTypings() { 97 | mark('BabelTypeScriptPlugin::generateTypings') 98 | const configPath = this.getTsConfigPath() 99 | 100 | await execAsync(`npx tsc -p ${configPath} --emitDeclarationOnly`) 101 | } 102 | 103 | private generatePrecompiledTsConfig() { 104 | mark('BabelTypeScriptPlugin::generatePrecompiledTsConfig') 105 | 106 | const config = this.getTsConfig() 107 | 108 | if (!config.options.rootDir || !config.options.baseUrl) { 109 | throw new Error('A rootDir or baseUrl not defined for tsconfig.') 110 | } 111 | 112 | const rootDir = relative(config.options.baseUrl, config.options.rootDir) 113 | 114 | const precompiledConfig = { 115 | extends: this.options.tsConfigPath, 116 | compilerOptions: { 117 | paths: {}, 118 | rootDir: this.precompiledDir, 119 | allowJs: true, 120 | declaration: false, 121 | }, 122 | include: config.raw.include.map((path: string) => path.replace(rootDir, this.precompiledDir)), 123 | exclude: config.raw.exclude.map((path: string) => path.replace(rootDir, this.precompiledDir)), 124 | } 125 | 126 | writeJSONSync(this.precompiledConfigPath, precompiledConfig, { spaces: 2 }) 127 | } 128 | 129 | private getTsConfigPath() { 130 | const configName = this.options.tsConfigPath 131 | const configPath = ts.findConfigFile(this.context, ts.sys.fileExists, configName) as string 132 | 133 | if (!configPath) { 134 | throw new Error('Cannot find tsconfig, please check path.') 135 | } 136 | 137 | return configPath 138 | } 139 | 140 | private getTsConfig() { 141 | const configPath = this.getTsConfigPath() 142 | const configFile = ts.readConfigFile(configPath, ts.sys.readFile) 143 | const compilerOptions = ts.parseJsonConfigFileContent(configFile.config, ts.sys, this.context) 144 | 145 | return compilerOptions 146 | } 147 | 148 | // TODO: Move this logic to separate plugin. 149 | private async generateModulePackage(): Promise { 150 | const src = this.output 151 | const files = await glob('**/index.js', { cwd: src }) 152 | 153 | for (const file of files) { 154 | const moduleDirname = dirname(file) 155 | const esmModuleDirname = dirname(join('esm', file)) 156 | const packageJsonPath = resolve(src, moduleDirname, 'package.json') 157 | const json: { sideEffects: string[] | boolean; module?: string } = { 158 | sideEffects: ['*.css', '*@desktop.js', '*@touch-phone.js', '*@touch-pad.js'], 159 | } 160 | 161 | if (this.options.onCreateSideEffects !== undefined) { 162 | const sideEffects = this.options.onCreateSideEffects(file) 163 | if (sideEffects !== undefined) { 164 | json.sideEffects = sideEffects 165 | } 166 | } 167 | 168 | if (file.match(/^esm/) === null) { 169 | json.module = join(relative(moduleDirname, esmModuleDirname), 'index.js') 170 | } 171 | 172 | await writeJson(packageJsonPath, json, { spaces: 2 }) 173 | } 174 | } 175 | } 176 | 177 | export function useBabelTypeScriptPlugin(options: Options): BabelTypeScriptPlugin { 178 | return new BabelTypeScriptPlugin(options) 179 | } 180 | -------------------------------------------------------------------------------- /packages/pack/README.md: -------------------------------------------------------------------------------- 1 | # @bem-react/pack · [![npm (scoped)](https://img.shields.io/npm/v/@bem-react/pack.svg)](https://www.npmjs.com/package/@bem-react/pack) 2 | 3 | A tool for building and prepare components for publishing. 4 | 5 | ## ✈️ Install 6 | 7 | via npm: 8 | 9 | ```sh 10 | npm i -DE @bem-react/pack 11 | ``` 12 | 13 | via yarn: 14 | 15 | ```sh 16 | yarn add -D @bem-react/pack 17 | ``` 18 | 19 | ## ☄️ Usage 20 | 21 | ```sh 22 | Runs components build with defined plugins. 23 | 24 | USAGE 25 | $ pack build 26 | 27 | OPTIONS 28 | -c, --config=config [default: build.config.json] The path to a build config file. 29 | ``` 30 | 31 | ## ⚙️ Configuration 32 | 33 | An example configuration: 34 | 35 | ```js 36 | const { resolve } = require('path') 37 | const { useCleanUpPlugin } = require('@bem-react/pack/lib/plugins/CleanUpPlugin') 38 | const { useCopyAssetsPlugin } = require('@bem-react/pack/lib/plugins/CopyAssetsPlugin') 39 | const { useCssPlugin } = require('@bem-react/pack/lib/plugins/CssPlugin') 40 | const { useTypeScriptPlugin } = require('@bem-react/pack/lib/plugins/TypescriptPlugin') 41 | 42 | /** 43 | * @type {import('@bem-react/pack/lib/interfaces').Config} 44 | */ 45 | module.exports = { 46 | context: resolve(__dirname, '..'), 47 | 48 | output: './dist', 49 | 50 | plugins: [ 51 | useCleanUpPlugin(['./dist']), 52 | 53 | useTypeScriptPlugin({ 54 | configPath: './tsconfig.prod.json', 55 | }), 56 | 57 | useCssPlugin({ 58 | context: './src', 59 | src: './**/*.css', 60 | output: ['./dist', './dist/esm'], 61 | }), 62 | 63 | useCopyAssetsPlugin([ 64 | { 65 | context: './src', 66 | src: './**/*.{svg,md,json}', 67 | output: ['./dist', './dist/esm'], 68 | }, 69 | ]), 70 | ], 71 | } 72 | ``` 73 | 74 | ### Declaration 75 | 76 | ```ts 77 | type Config = { 78 | /** 79 | * Executing context. 80 | * 81 | * @default cwd 82 | */ 83 | context?: string 84 | 85 | /** 86 | * Output directory. 87 | */ 88 | output: string 89 | 90 | /** 91 | * Plugins list. 92 | */ 93 | plugins: Plugin[] 94 | } 95 | ``` 96 | 97 | ## 🛠 Plugins 98 | 99 | ### CleanUpPlugin 100 | 101 | Plugin for cleanuping directories. _(Run at `beforeRun` step)._ 102 | 103 | #### Usage 104 | 105 | ```js 106 | const { useCleanUpPlugin } = require('@bem-react/pack/lib/plugins/CleanUpPlugin') 107 | 108 | useCleanUpPlugin(['./dist']) 109 | ``` 110 | 111 | #### Declaration 112 | 113 | ```ts 114 | /** 115 | * A list of directories which need to be cleaned. 116 | */ 117 | type Sources = string[] 118 | 119 | export declare function useCleanUpPlugin(sources: Sources): CleanUpPlugin 120 | ``` 121 | 122 | ### CopyAssetsPlugin 123 | 124 | Plugin for copying assets. _(Run at `afterRun` step)._ 125 | 126 | #### Usage 127 | 128 | ```js 129 | const { useCopyAssetsPlugin } = require('@bem-react/pack/lib/plugins/CopyAssetsPlugin') 130 | 131 | useCopyAssetsPlugin([ 132 | { 133 | context: './src', 134 | src: './**/*.{svg,md,json}', 135 | output: ['./dist', './dist/esm'], 136 | }, 137 | ]) 138 | ``` 139 | 140 | #### Declaration 141 | 142 | ```ts 143 | type Rule = { 144 | /** 145 | * Glob or path from where we сopy files. 146 | */ 147 | src: string 148 | 149 | /** 150 | * Output paths. 151 | */ 152 | output: string[] 153 | 154 | /** 155 | * A path that determines how to interpret the `src` path. 156 | */ 157 | context?: string 158 | 159 | /** 160 | * Paths to files that will be ignored when copying. 161 | */ 162 | ignore?: string[] 163 | } 164 | 165 | type Rules = Rule | Rule[] 166 | 167 | function useCopyAssetsPlugin(rules: Rules): CopyAssetsPlugin 168 | ``` 169 | 170 | ### CssPlugin 171 | 172 | A plugin that copies css files and makes processing using postcss on demand. _(Run at `run` step)._ 173 | 174 | #### Usage 175 | 176 | ```js 177 | const { useCssPlugin } = require('@bem-react/pack/lib/plugins/CssPlugin') 178 | 179 | useCssPlugin({ 180 | context: './src', 181 | src: './**/*.css', 182 | }) 183 | ``` 184 | 185 | #### Declaration 186 | 187 | ```ts 188 | type Options = { 189 | /** 190 | * A path that determines how to interpret the `src` path. 191 | */ 192 | context?: string 193 | /** 194 | * Glob or path from where we сopy files. 195 | */ 196 | src: string 197 | /** 198 | * Output paths. 199 | */ 200 | output: string[] 201 | /** 202 | * Paths to files that will be ignored when copying and processing. 203 | */ 204 | ignore?: string[] 205 | /** 206 | * A path to postcss config. 207 | */ 208 | postcssConfigPath?: string 209 | } 210 | 211 | export declare function useCssPlugin(options: Options): CssPlugin 212 | ``` 213 | 214 | ### TypescriptPlugin 215 | 216 | A plugin that process ts and creates two copies of the build (cjs and esm). _(Run at `run` step)._ 217 | 218 | #### Usage 219 | 220 | ```js 221 | const { useTypeScriptPlugin } = require('@bem-react/pack/lib/plugins/TypescriptPlugin') 222 | 223 | useTypeScriptPlugin({ 224 | configPath: './tsconfig.prod.json', 225 | }) 226 | ``` 227 | 228 | #### Declaration 229 | 230 | ```ts 231 | type Options = { 232 | /** 233 | * A path to typescript config. 234 | */ 235 | configPath?: string 236 | 237 | /** 238 | * A callback for when creating side effects. 239 | */ 240 | onCreateSideEffects: (path: string) => string[] | boolean | undefined 241 | } 242 | 243 | function useTypeScriptPlugin(options: Options): TypeScriptPlugin 244 | ``` 245 | 246 | ### PackageJsonPlugin 247 | 248 | A plugin that copy package.json and modify content. _(Run at `onFinish` step)._ 249 | 250 | #### Usage 251 | 252 | ```js 253 | const { usePackageJsonPlugin } = require('@bem-react/pack/lib/plugins/PackageJsonPlugin') 254 | 255 | usePackageJsonPlugin({ 256 | scripts: {}, 257 | }) 258 | ``` 259 | 260 | ## 🏗 Write own plugin 261 | 262 | The plugin can perform an action on one of the available hook `onBeforeRun`, `onRun` and `onAfterRun`. 263 | 264 | ### Example 265 | 266 | ```ts 267 | import { Plugin, OnDone, HookOptions } from '@bem-raect/pack/lib/interfaces' 268 | 269 | class MyPlugin implements Plugin { 270 | async onRun(done: OnDone, { context, output }: HookOptions) { 271 | // Do something stuff. 272 | done() 273 | } 274 | } 275 | 276 | export function useMyPlugin(): MyPlugin { 277 | return new MyPlugin() 278 | } 279 | ``` 280 | 281 | ### Declaration 282 | 283 | ```ts 284 | type OnDone = () => void 285 | type HookOptions = { context: string; output: string } 286 | type HookFn = (done: OnDone, options: HookOptions) => Promise 287 | 288 | interface Plugin { 289 | /** 290 | * Run hook at start. 291 | */ 292 | onStart?: HookFn 293 | 294 | /** 295 | * Run hook before run. 296 | */ 297 | onBeforeRun?: HookFn 298 | 299 | /** 300 | * Run hook at run. 301 | */ 302 | onRun?: HookFn 303 | 304 | /** 305 | * Run hook after run. 306 | */ 307 | onAfterRun?: HookFn 308 | 309 | /** 310 | * Run hook at finish. 311 | */ 312 | onFinish?: HookFn 313 | } 314 | ``` 315 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint", "react-hooks", "react"], 4 | "env": { 5 | "browser": true, 6 | "es6": true, 7 | "node": true 8 | }, 9 | "globals": { 10 | "__DEV__": true 11 | }, 12 | "parserOptions": { 13 | "ecmaVersion": 2018, 14 | "sourceType": "module", 15 | "ecmaFeatures": { "jsx": true }, 16 | "useJSXTextNode": true 17 | }, 18 | "settings": { 19 | "react": { 20 | "pragma": "React", 21 | "version": "16.0" 22 | } 23 | }, 24 | "rules": { 25 | // see https://github.com/airbnb/javascript/blob/master/packages/eslint-config-airbnb-base/rules/style.js#L122 26 | "indent": [ 27 | 2, 28 | 2, 29 | { 30 | "SwitchCase": 1, 31 | "VariableDeclarator": 1, 32 | "outerIIFEBody": 1, 33 | "FunctionDeclaration": { 34 | "parameters": 1, 35 | "body": 1 36 | }, 37 | "FunctionExpression": { 38 | "parameters": 1, 39 | "body": 1 40 | }, 41 | "CallExpression": { 42 | "arguments": 1 43 | }, 44 | "ArrayExpression": 1, 45 | "ObjectExpression": 1, 46 | "ImportDeclaration": 1, 47 | "flatTernaryExpressions": false, 48 | "ignoreComments": false 49 | } 50 | ], 51 | 52 | "semi": [2, "never"], 53 | "semi-spacing": [2, { "before": false, "after": true }], 54 | "wrap-iife": [2, "inside"], 55 | "no-use-before-define": [2, { "functions": true, "classes": true, "variables": true }], 56 | "no-caller": 2, 57 | "no-cond-assign": [2, "except-parens"], 58 | "no-constant-condition": 2, 59 | "no-debugger": 2, 60 | "no-dupe-args": 2, 61 | "no-dupe-keys": 2, 62 | "no-duplicate-case": 2, 63 | "no-empty": [2, { "allowEmptyCatch": true }], 64 | "no-extra-boolean-cast": 2, 65 | "no-extra-semi": 2, 66 | "no-func-assign": 2, 67 | "no-new": 2, 68 | "no-sparse-arrays": 2, 69 | "no-undef": 2, 70 | "no-unexpected-multiline": 2, 71 | "no-unreachable": 2, 72 | "no-unused-vars": [ 73 | 2, 74 | { 75 | "args": "after-used", 76 | "argsIgnorePattern": "^_", 77 | "ignoreRestSiblings": true, 78 | "vars": "all", 79 | "varsIgnorePattern": "^_" 80 | } 81 | ], 82 | 83 | "strict": 2, 84 | "max-params": [2, 5], 85 | "max-depth": [1, 4], 86 | "no-eq-null": 0, 87 | "no-unused-expressions": 0, 88 | "dot-notation": 2, 89 | "use-isnan": 2, 90 | 91 | // Best practices 92 | "block-scoped-var": 2, 93 | "complexity": [0, 11], 94 | "curly": [2, "multi-line"], 95 | "eqeqeq": [2, "always", { "null": "ignore" }], 96 | "no-else-return": 2, 97 | "no-extra-bind": 2, 98 | "no-implicit-coercion": 2, 99 | "no-return-assign": 0, 100 | "no-sequences": 2, 101 | "yoda": 2, 102 | 103 | // Variables 104 | "no-restricted-globals": [2, "fdescribe", "fit"], 105 | "no-var": 1, 106 | 107 | // Codestyle 108 | "arrow-parens": [2, "always"], 109 | "array-bracket-spacing": [2, "never"], 110 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 111 | "camelcase": [2, { "properties": "never" }], 112 | "comma-dangle": ["warn", "always-multiline"], 113 | "comma-spacing": [2, { "before": false, "after": true }], 114 | "eol-last": 2, 115 | "func-call-spacing": [2, "never"], 116 | "block-spacing": 2, 117 | "keyword-spacing": [2, { "before": true, "after": true }], 118 | "max-len": [ 119 | 2, 120 | { 121 | "code": 120, 122 | "ignoreUrls": true, 123 | "ignoreComments": false, 124 | "ignoreRegExpLiterals": true, 125 | "ignoreStrings": true, 126 | "ignoreTemplateLiterals": true, 127 | "ignorePattern": "require" 128 | } 129 | ], 130 | "no-lonely-if": 2, 131 | "no-mixed-spaces-and-tabs": 2, 132 | "no-multi-spaces": 2, 133 | "no-multiple-empty-lines": [2, { "max": 1, "maxBOF": 0, "maxEOF": 0 }], 134 | "no-trailing-spaces": 2, 135 | "no-unneeded-ternary": 2, 136 | "no-nested-ternary": 2, 137 | "object-curly-spacing": [2, "always"], 138 | "one-var-declaration-per-line": [2, "initializations"], 139 | "one-var": [2, { "let": "never", "const": "never" }], 140 | "operator-linebreak": [2, "before"], 141 | "padded-blocks": [2, "never"], 142 | "quote-props": [2, "as-needed", { "numbers": true }], 143 | "quotes": [2, "single", { "avoidEscape": true }], 144 | "space-before-blocks": [2, "always"], 145 | "space-before-function-paren": [ 146 | 2, 147 | { 148 | "asyncArrow": "always", 149 | "anonymous": "never", 150 | "named": "never" 151 | } 152 | ], 153 | "space-in-parens": 2, 154 | "no-console": [2, { "allow": ["assert", "error", "warn"] }], 155 | "key-spacing": [2, { "beforeColon": false, "afterColon": true, "mode": "strict" }], 156 | "space-infix-ops": 2, 157 | 158 | // REACT 159 | // https://github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules 160 | "jsx-quotes": [2, "prefer-double"], 161 | "react/jsx-boolean-value": 2, 162 | "react/display-name": 0, 163 | "react/jsx-closing-tag-location": 2, 164 | "react/jsx-equals-spacing": 2, 165 | "react/jsx-first-prop-new-line": [2, "multiline"], 166 | "react/jsx-handler-names": 0, 167 | "react/jsx-indent": [2, 2], 168 | "react/jsx-indent-props": [2, 2], 169 | "react/jsx-key": 2, 170 | "react/jsx-no-bind": 1, 171 | "react/jsx-no-duplicate-props": 2, 172 | "react/jsx-no-literals": 0, 173 | "react/jsx-no-undef": 2, 174 | "react/jsx-sort-props": 0, 175 | "react/jsx-tag-spacing": [2, { "beforeClosing": "never", "beforeSelfClosing": "always" }], 176 | "react/jsx-uses-react": 2, 177 | "react/jsx-uses-vars": 2, 178 | "react/no-find-dom-node": 2, 179 | "react/no-multi-comp": 0, 180 | "react/no-set-state": 0, 181 | "react/react-in-jsx-scope": 2, 182 | "react/require-optimization": 0, 183 | "react/self-closing-comp": 2, 184 | "react/style-prop-object": 2, 185 | "react/void-dom-elements-no-children": 2, 186 | 187 | // https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#supported-rules 188 | "@typescript-eslint/consistent-type-assertions": 2, 189 | "@typescript-eslint/no-empty-interface": 2, 190 | "@typescript-eslint/no-unused-vars": [ 191 | 2, 192 | { 193 | "args": "after-used", 194 | "argsIgnorePattern": "^_", 195 | "ignoreRestSiblings": true, 196 | "vars": "all", 197 | "varsIgnorePattern": "^_" 198 | } 199 | ], 200 | 201 | // https://reactjs.org/docs/hooks-rules.html 202 | "react-hooks/rules-of-hooks": 2, // Checks rules of Hooks 203 | "react-hooks/exhaustive-deps": 1 // Checks effect dependencies 204 | }, 205 | "overrides": [ 206 | { 207 | "files": ["*.test.{ts,tsx,js}"], 208 | "env": { 209 | "jest": true 210 | } 211 | }, 212 | { 213 | "files": ["*.{js,cjs}"], 214 | "rules": { 215 | "strict": 0 216 | } 217 | } 218 | ] 219 | } 220 | -------------------------------------------------------------------------------- /packages/di/di.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | ReactNode, 3 | FC, 4 | ComponentType, 5 | createContext, 6 | useContext, 7 | useRef, 8 | createElement, 9 | } from 'react' 10 | 11 | export type RegistryContext = Record 12 | 13 | export const registryContext = createContext({}) 14 | const RegistriesConsumer = registryContext.Consumer 15 | const RegistryProvider = registryContext.Provider 16 | 17 | export function withRegistry( 18 | ...registries: Registry[] 19 | ):

(Component: ComponentType

) => FC

20 | export function withRegistry() { 21 | // Use arguments instead of rest-arguments to get faster and more compact code. 22 | const registries: Registry[] = [].slice.call(arguments) 23 | 24 | return function WithRegistry

(Component: ComponentType

) { 25 | const RegistryResolver: FC

= (props) => { 26 | const providedRegistriesRef = useRef(null) 27 | 28 | return ( 29 | 30 | {(contextRegistries) => { 31 | if (providedRegistriesRef.current === null) { 32 | const providedRegistries = { ...contextRegistries } 33 | 34 | for (let i = 0; i < registries.length; i++) { 35 | const registry = registries[i] 36 | const overrides = providedRegistries[registry.id] 37 | // eslint-disable-next-line no-nested-ternary 38 | providedRegistries[registry.id] = registry.overridable 39 | ? overrides 40 | ? registry.merge(overrides) 41 | : registry 42 | : registry && overrides 43 | ? overrides.merge(registry) 44 | : registry 45 | } 46 | 47 | providedRegistriesRef.current = providedRegistries 48 | } 49 | 50 | return ( 51 | 52 | {/* Use createElement instead of jsx to avoid __assign from tslib. */} 53 | {createElement(Component, props)} 54 | 55 | ) 56 | }} 57 | 58 | ) 59 | } 60 | 61 | if (__DEV__) { 62 | const resolverValue = registries.map((registry) => registry.id).join(', ') 63 | // TODO: Use setDisplayName util. 64 | RegistryResolver.displayName = `RegistryResolver(${resolverValue})` 65 | } 66 | 67 | return RegistryResolver 68 | } 69 | } 70 | 71 | export interface IRegistryConsumerProps { 72 | id: string 73 | children: (registry: any) => ReactNode 74 | } 75 | 76 | export const RegistryConsumer: FC = (props) => ( 77 | 78 | {(registries) => { 79 | if (__DEV__) { 80 | if (!registries[props.id]) { 81 | throw new Error(`Registry with id '${props.id}' not found.`) 82 | } 83 | } 84 | 85 | return props.children(registries[props.id].snapshot()) 86 | }} 87 | 88 | ) 89 | 90 | /** 91 | * @deprecated consider using 'RegistryConsumer' instead 92 | */ 93 | export const ComponentRegistryConsumer = RegistryConsumer 94 | 95 | export const useRegistries = () => { 96 | return useContext(registryContext) 97 | } 98 | 99 | export function useRegistry(id: string) { 100 | const registries = useRegistries() 101 | 102 | return registries[id].snapshot() 103 | } 104 | 105 | /** 106 | * @deprecated consider using 'useRegistry' instead 107 | */ 108 | export const useComponentRegistry = useRegistry 109 | 110 | export interface IRegistryOptions { 111 | id: string 112 | overridable?: boolean 113 | } 114 | 115 | const registryOverloadMark = 'RegistryOverloadMark' 116 | 117 | type SimpleOverload = (Base: T) => T 118 | 119 | interface IRegistryEntityOverload { 120 | $symbol: typeof registryOverloadMark 121 | overload: SimpleOverload 122 | } 123 | 124 | type IRegistryEntity = T | IRegistryEntityOverload 125 | export type IRegistryEntities = Record 126 | 127 | function withOverload(overload: SimpleOverload): IRegistryEntityOverload { 128 | return { 129 | $symbol: registryOverloadMark, 130 | overload, 131 | } 132 | } 133 | 134 | function isOverload(entity: IRegistryEntity): entity is IRegistryEntityOverload { 135 | return (entity as IRegistryEntityOverload).$symbol === registryOverloadMark 136 | } 137 | 138 | export class Registry { 139 | id: string 140 | overridable: boolean 141 | private entities: IRegistryEntities = {} 142 | 143 | constructor({ id, overridable = true }: IRegistryOptions) { 144 | this.id = id 145 | this.overridable = overridable 146 | } 147 | 148 | /** 149 | * Set registry entry by id. 150 | * 151 | * @param id entry id 152 | * @param entity valid registry entity 153 | */ 154 | set(id: string, entity: T) { 155 | this.entities[id] = entity 156 | 157 | return this 158 | } 159 | 160 | /** 161 | * Set extender for registry entry by id. 162 | * 163 | * @param id entry id 164 | * @param overload valid registry entity extender 165 | */ 166 | extends(id: string, overload: SimpleOverload) { 167 | this.entities[id] = withOverload(overload) 168 | 169 | return this 170 | } 171 | 172 | /** 173 | * Set react entities in registry via object literal. 174 | * 175 | * @param entitiesSet set of valid registry entities 176 | */ 177 | fill(entitiesSet: IRegistryEntities) { 178 | for (const key in entitiesSet) { 179 | this.entities[key] = entitiesSet[key] 180 | } 181 | 182 | return this 183 | } 184 | 185 | /** 186 | * Get entry from registry by id. 187 | * 188 | * @param id entry id 189 | */ 190 | get(id: string): IRegistryEntity { 191 | if (__DEV__) { 192 | if (!this.entities[id]) { 193 | throw new Error(`Entry with id '${id}' not found.`) 194 | } 195 | } 196 | 197 | return this.entities[id] 198 | } 199 | 200 | /** 201 | * Returns raw entities from registry. 202 | */ 203 | snapshot(): IRegistryEntities { 204 | return this.entities 205 | } 206 | 207 | /** 208 | * Override entities by external registry. 209 | * @internal 210 | * 211 | * @param otherRegistry external registry 212 | */ 213 | merge(otherRegistry?: Registry) { 214 | const clone = new Registry({ id: this.id, overridable: this.overridable }) 215 | clone.fill(this.entities) 216 | 217 | if (!otherRegistry) return clone 218 | 219 | const otherRegistryEntities = otherRegistry.snapshot() 220 | 221 | for (const entityName in otherRegistryEntities) { 222 | if (!otherRegistryEntities.hasOwnProperty(entityName)) continue 223 | 224 | clone.entities[entityName] = this.mergeEntities( 225 | clone.entities[entityName], 226 | otherRegistryEntities[entityName], 227 | ) 228 | } 229 | 230 | return clone 231 | } 232 | 233 | /** 234 | * Returns extended or replaced entity 235 | * 236 | * @param base base implementation 237 | * @param overrides overridden implementation 238 | */ 239 | private mergeEntities(base: IRegistryEntity, overrides: IRegistryEntity): IRegistryEntity { 240 | if (isOverload(overrides)) { 241 | if (!base) return overrides 242 | 243 | if (isOverload(base)) { 244 | // If both entities are hocs, then create compose-hoc 245 | return withOverload((Base) => overrides.overload(base.overload(Base))) 246 | } 247 | 248 | return overrides.overload(base) 249 | } 250 | 251 | return overrides 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /packages/di/README.md: -------------------------------------------------------------------------------- 1 | # @bem-react/di · [![npm (scoped)](https://img.shields.io/npm/v/@bem-react/di.svg)](https://www.npmjs.com/package/@bem-react/di) [![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/@bem-react/di.svg)](https://bundlephobia.com/result?p=@bem-react/di) 2 | 3 | **Dependency Injection (DI)** allows you to split React components into separate versions and comfortably switch them in the project whenever needed, e.g., to make a specific bundle. 4 | 5 | DI package helps to solve similar tasks with minimum effort: 6 | 7 | - decouple _desktop_ and _mobile_ versions of a component 8 | - implement an _experimental_ version of a component alongside the common one 9 | - store components and their auxiliaries (like settings and functions) in a single place 10 | 11 | ## Install 12 | 13 | ``` 14 | npm i -S @bem-react/di 15 | ``` 16 | 17 | ## Quick start 18 | 19 | **Note!** This example uses [@bem-react/classname package](https://github.com/bem/bem-react/tree/master/packages/classname). 20 | 21 | E.g., for a structure like this: 22 | 23 | ``` 24 | Components/ 25 | Header/ 26 | Header@desktop.tsx 27 | Header@mobile.tsx 28 | Footer/ 29 | Footer@desktop.tsx 30 | Footer@mobile.tsx 31 | App.tsx 32 | ``` 33 | 34 | First, create two files that define two versions of the App and use different sets of components: `App@desktop.tsx` and `App@mobile.tsx`. Put them near `App.tsx`. 35 | 36 | In each App version (`App@desktop.tsx` and `App@mobile.tsx`) we should define which components should be used. 37 | Three steps to do this: 38 | 39 | 1. Create a registry with a particular id: 40 | 41 | ```ts 42 | const registry = new Registry({ id: cnApp() }) 43 | ``` 44 | 45 | 2. Register all the needed components versions under a descriptive key (keys, describing similar components, should be the same across all the versions): 46 | 47 | ```ts 48 | registry.set('Header', Header) 49 | registry.set('Footer', Footer) 50 | ``` 51 | 52 | or 53 | 54 | ```ts 55 | registry.fill({ 56 | Header, 57 | Footer, 58 | }) 59 | ``` 60 | 61 | or 62 | 63 | ```ts 64 | registry.fill({ 65 | 'id-1': Header, 66 | 'id-2': Footer, 67 | }) 68 | ``` 69 | 70 | 3. Export the App version with its registry of components: 71 | 72 | ```ts 73 | export const AppNewVersion = withRegistry(registry)(AppCommon) 74 | ``` 75 | 76 | The files should look like this: 77 | 78 | **1.** In `App.tsx` 79 | 80 | ```tsx 81 | import { cn } from '@bem-react/classname' 82 | 83 | export const cnApp = cn('App') 84 | export const registryId = cnApp() 85 | ``` 86 | 87 | **2.** In `App@desktop.tsx` 88 | 89 | ```tsx 90 | import { Registry, withRegistry } from '@bem-react/di' 91 | import { App as AppCommon, registryId } from './App' 92 | 93 | import { Footer } from './Components/Footer/Footer@desktop' 94 | import { Header } from './Components/Header/Header@desktop' 95 | 96 | export const registry = new Registry({ id: registryId }) 97 | 98 | registry.set('Header', Header) 99 | registry.set('Footer', Footer) 100 | 101 | export const AppDesktop = withRegistry(registry)(AppCommon) 102 | ``` 103 | 104 | **3.** In `App@mobile.tsx` 105 | 106 | ```tsx 107 | import { Registry, withRegistry } from '@bem-react/di' 108 | import { App as AppCommon, registryId } from './App' 109 | 110 | import { Footer } from './Components/Footer/Footer@mobile' 111 | import { Header } from './Components/Header/Header@mobile' 112 | 113 | export const registry = new Registry({ id: registryId }) 114 | 115 | registry.set('Header', Header) 116 | registry.set('Footer', Footer) 117 | 118 | export const AppMobile = withRegistry(registry)(AppCommon) 119 | ``` 120 | 121 | Time to use these versions in your app dynamically! 122 | 123 | If in `App.tsx` your dependencies were static before 124 | 125 | ```tsx 126 | import React from 'react' 127 | import { cn } from '@bem-react/classname' 128 | import { Header } from './Components/Header/Header' 129 | import { Footer } from './Components/Footer/Footer' 130 | 131 | export const App = () => ( 132 | <> 133 |

134 |
135 | 136 | ) 137 | ``` 138 | 139 | Now the dependencies can be injected based on the currently used registry 140 | 141 | with `RegistryConsumer` 142 | 143 | ```tsx 144 | import React from 'react' 145 | import { cn } from '@bem-react/classname' 146 | import { RegistryConsumer } from '@bem-react/di' 147 | 148 | // No Header or Footer imports 149 | 150 | const cnApp = cn('App') 151 | 152 | export const App = () => ( 153 | 154 | {({ Header, Footer }) => ( 155 | <> 156 |
157 |
158 | 159 | )} 160 | 161 | ) 162 | ``` 163 | 164 | with `useRegistry` (_require react version 16.8.0+_) 165 | 166 | ```tsx 167 | import React from 'react' 168 | import { cn } from '@bem-react/classname' 169 | import { useRegistry } from '@bem-react/di' 170 | 171 | // No Header or Footer imports 172 | 173 | const cnApp = cn('App') 174 | 175 | export const App = () => { 176 | const { Header, Footer } = useRegistry(cnApp()) 177 | 178 | return ( 179 | <> 180 |
181 |
182 | 183 | ) 184 | } 185 | ``` 186 | 187 | So you could use different versions of your app e.g. for conditional rendering on your server side or to create separate bundles 188 | 189 | ```ts 190 | import { AppDesktop } from './path-to/App@desktop' 191 | import { AppMobile } from './path-to/App@mobile' 192 | ``` 193 | 194 | ## Replacing components 195 | 196 | Components inside registry can be replaced (e.g. for experiments) by wrapping `withRegistry(...)(App)` with another registry. 197 | 198 | ```ts 199 | import { Registry, withRegistry } from '@bem-react/di' 200 | 201 | import { AppDesktop, registryId } from './App@desktop' 202 | import { HeaderExperimental } from './experiments/Components/Header/Header' 203 | 204 | const expRegistry = new Registry({ id: registryId }) 205 | 206 | // replacing original Header with HeaderExperimental 207 | expRegistry.set('Header', HeaderExperimental) 208 | 209 | // AppDesktopExperimental will call App with HeaderExperimental as 'Header' 210 | export const AppDesktopExperimental = withRegistry(expRegistry)(AppDesktop) 211 | ``` 212 | 213 | When `App` extracts components from registry _DI_ actually takes all registries defined above and merges. By default higher defined registry overrides lower defined one. 214 | 215 | If at some point you want to create registry that wan't be overrided just call the constructor with `overridable: false`. 216 | 217 | ```ts 218 | const boldRegistry = new Registry({ id: cnApp(), overridable: false }) 219 | ``` 220 | 221 | ## Extending components 222 | 223 | You can extend (e.g. for experiments) a component using method `extends(...)` in overridden registry. 224 | 225 | ```tsx 226 | import { Registry, withRegistry, withBase } from '@bem-react/di' 227 | import { AppDesktop, registryId } from './App@desktop' 228 | 229 | const expRegistry = new Registry({ id: registryId }) 230 | 231 | // extends original Header 232 | expRegistry.extends('Header', (BaseHeader) => (props) => ( 233 |
234 | 235 |
236 | )) 237 | 238 | // AppDesktopExperimental will call App with extended 'Header' 239 | export const AppDesktopExperimental = withRegistry(expRegistry)(AppDesktop) 240 | ``` 241 | 242 | _DI_ merges nested registries composing and ordinary components for you. So you always can get a reference to previous component's implementation. 243 | 244 | ## Storing other 245 | 246 | _DI_ registry may keep not only components but also their settings and any other auxiliaries (like functions). 247 | 248 | ```tsx 249 | import { useRegistry } from '@bem-react/di' 250 | 251 | const cnHeader = cn('Header') 252 | 253 | export const Header = (props) => { 254 | const { theme, showNotification, prepareProps } = useRegistry(cnApp()) 255 | 256 | // one function is used to fulfill props 257 | const { title, username } = prepareProps(props) 258 | 259 | useEffect(() => { 260 | // another function is used inside hook 261 | showNotification() 262 | }) 263 | 264 | return ( 265 |
266 |

{title}

267 |

Greetings ${username}

268 |
269 | ) 270 | } 271 | ``` 272 | -------------------------------------------------------------------------------- /packages/classname/test/classname.test.ts: -------------------------------------------------------------------------------- 1 | import { cn, withNaming } from '../classname' 2 | 3 | describe('@bem-react/classname', () => { 4 | describe('cn', () => { 5 | test('block', () => { 6 | const b = cn('Block') 7 | expect(b()).toEqual('Block') 8 | }) 9 | 10 | test('elem', () => { 11 | const e = cn('Block', 'Elem') 12 | expect(e()).toEqual('Block-Elem') 13 | }) 14 | 15 | describe('modifiers', () => { 16 | test('block', () => { 17 | const b = cn('Block') 18 | expect(b({ modName: true })).toEqual('Block Block_modName') 19 | }) 20 | 21 | test('elem', () => { 22 | const e = cn('Block', 'Elem') 23 | expect(e({ modName: true })).toEqual('Block-Elem Block-Elem_modName') 24 | }) 25 | 26 | test('more than one', () => { 27 | const mods = { modName: true, modName2: 'modVal' } 28 | const b = cn('Block') 29 | const e = cn('Block', 'Elem') 30 | 31 | expect(b(mods)).toEqual('Block Block_modName Block_modName2_modVal') 32 | expect(e(mods)).toEqual('Block-Elem Block-Elem_modName Block-Elem_modName2_modVal') 33 | }) 34 | 35 | test('empty', () => { 36 | const b = cn('Block') 37 | expect(b({})).toEqual('Block') 38 | }) 39 | 40 | test('falsy', () => { 41 | const b = cn('Block') 42 | expect(b({ modName: false })).toEqual('Block') 43 | }) 44 | 45 | test('with falsy', () => { 46 | const b = cn('Block', 'Elem') 47 | expect(b({ modName: false, mod: 'val' })).toEqual('Block-Elem Block-Elem_mod_val') 48 | }) 49 | 50 | test('zero', () => { 51 | const b = cn('Block') 52 | expect(b({ modName: '0' })).toEqual('Block Block_modName_0') 53 | }) 54 | 55 | test('undefined', () => { 56 | const b = cn('Block') 57 | expect(b({ modName: undefined })).toEqual('Block') 58 | }) 59 | }) 60 | 61 | describe('mix', () => { 62 | test('block', () => { 63 | const b = cn('Block') 64 | expect(b(null, 'Mix')).toEqual('Block Mix') 65 | expect(b(null, ['Mix1', 'Mix2'])).toEqual('Block Mix1 Mix2') 66 | }) 67 | 68 | test('block with mods', () => { 69 | const b = cn('Block') 70 | expect(b({ theme: 'normal' }, 'Mix')).toEqual('Block Block_theme_normal Mix') 71 | expect(b({ theme: 'normal' }, ['Mix'])).toEqual('Block Block_theme_normal Mix') 72 | }) 73 | 74 | test('elem', () => { 75 | const e = cn('Block', 'Elem') 76 | expect(e(null, 'Mix')).toEqual('Block-Elem Mix') 77 | expect(e(null, ['Mix1', 'Mix2'])).toEqual('Block-Elem Mix1 Mix2') 78 | }) 79 | 80 | test('elem with mods', () => { 81 | const e = cn('Block', 'Elem') 82 | expect(e({ theme: 'normal' }, 'Mix')).toEqual('Block-Elem Block-Elem_theme_normal Mix') 83 | expect(e({ theme: 'normal' }, ['Mix'])).toEqual('Block-Elem Block-Elem_theme_normal Mix') 84 | }) 85 | 86 | test('carry elem', () => { 87 | const b = cn('Block') 88 | expect(b('Elem', 'Mix')).toEqual('Block-Elem Mix') 89 | expect(b('Elem', ['Mix1', 'Mix2'])).toEqual('Block-Elem Mix1 Mix2') 90 | }) 91 | 92 | test('carry elem with mods', () => { 93 | const b = cn('Block') 94 | expect(b('Elem', { theme: 'normal' }, 'Mix')).toEqual( 95 | 'Block-Elem Block-Elem_theme_normal Mix', 96 | ) 97 | expect(b('Elem', { theme: 'normal' }, ['Mix'])).toEqual( 98 | 'Block-Elem Block-Elem_theme_normal Mix', 99 | ) 100 | }) 101 | 102 | test('undefined', () => { 103 | const b = cn('Block') 104 | expect(b('Elem', null, undefined)).toEqual('Block-Elem') 105 | expect(b('Elem', null, [undefined])).toEqual('Block-Elem') 106 | }) 107 | 108 | /** @see https://github.com/bem/bem-react/issues/445 */ 109 | test('not string and not undefined', () => { 110 | const b = cn('Block') 111 | expect(b('Elem', null, false as any)).toEqual('Block-Elem') 112 | expect(b('Elem', null, true as any)).toEqual('Block-Elem') 113 | expect(b('Elem', null, 10 as any)).toEqual('Block-Elem') 114 | expect(b('Elem', null, null as any)).toEqual('Block-Elem') 115 | expect(b('Elem', null, [false as any])).toEqual('Block-Elem') 116 | expect(b('Elem', null, [true as any])).toEqual('Block-Elem') 117 | expect(b('Elem', null, [10 as any])).toEqual('Block-Elem') 118 | expect(b('Elem', null, [null as any])).toEqual('Block-Elem') 119 | }) 120 | 121 | test('unique block', () => { 122 | const b = cn('Block') 123 | expect(b(null, 'Block')).toEqual('Block') 124 | expect(b(null, ['Block'])).toEqual('Block') 125 | }) 126 | 127 | test('unique block with mods', () => { 128 | const b = cn('Block') 129 | expect(b({ theme: 'normal' }, 'Block Block_size_m')).toEqual( 130 | 'Block Block_theme_normal Block_size_m', 131 | ) 132 | expect(b({ theme: 'normal' }, ['Block Block_size_m'])).toEqual( 133 | 'Block Block_theme_normal Block_size_m', 134 | ) 135 | }) 136 | 137 | test('unique elem', () => { 138 | const b = cn('Block') 139 | expect(b('Elem', null, 'Block-Elem')).toEqual('Block-Elem') 140 | expect(b('Elem', null, ['Block-Elem'])).toEqual('Block-Elem') 141 | }) 142 | 143 | test('unique elem with mods', () => { 144 | const b = cn('Block') 145 | expect(b('Elem', { theme: 'normal' }, 'Block-Elem Block-Elem_size_m')).toEqual( 146 | 'Block-Elem Block-Elem_theme_normal Block-Elem_size_m', 147 | ) 148 | expect(b('Elem', { theme: 'normal' }, ['Block-Elem Block-Elem_size_m'])).toEqual( 149 | 'Block-Elem Block-Elem_theme_normal Block-Elem_size_m', 150 | ) 151 | }) 152 | 153 | test('object with valueOf', () => { 154 | const b = cn('Block') 155 | expect(b('Elem', null, { valueOf: () => 'Mix' } as string)).toEqual('Block-Elem Mix') 156 | expect(b('Elem', null, [{ valueOf: () => 'Mix' } as string])).toEqual('Block-Elem Mix') 157 | }) 158 | }) 159 | }) 160 | 161 | describe('withNaming origin preset', () => { 162 | const cCn = withNaming({ 163 | e: '__', 164 | m: '_', 165 | }) 166 | 167 | test('block', () => { 168 | const b = cCn('block') 169 | expect(b()).toEqual('block') 170 | }) 171 | 172 | test('elem', () => { 173 | const e = cCn('block', 'elem') 174 | expect(e()).toEqual('block__elem') 175 | }) 176 | 177 | describe('modifiers', () => { 178 | test('block', () => { 179 | const b = cCn('block') 180 | expect(b({ modName: true })).toEqual('block block_modName') 181 | }) 182 | 183 | test('elem', () => { 184 | const e = cCn('block', 'elem') 185 | expect(e({ modName: true })).toEqual('block__elem block__elem_modName') 186 | }) 187 | 188 | test('more than one', () => { 189 | const mods = { modName: true, modName2: 'modVal' } 190 | const b = cCn('block') 191 | const e = cCn('block', 'elem') 192 | 193 | expect(b(mods)).toEqual('block block_modName block_modName2_modVal') 194 | expect(e(mods)).toEqual('block__elem block__elem_modName block__elem_modName2_modVal') 195 | }) 196 | 197 | test('empty', () => { 198 | const b = cCn('block') 199 | expect(b({})).toEqual('block') 200 | }) 201 | 202 | test('falsy', () => { 203 | const b = cCn('block') 204 | expect(b({ modName: false })).toEqual('block') 205 | }) 206 | 207 | test('with falsy', () => { 208 | const b = cCn('block') 209 | expect(b({ modName: false, mod: 'val' })).toEqual('block block_mod_val') 210 | }) 211 | 212 | test('zero', () => { 213 | const b = cCn('block') 214 | expect(b({ modName: '0' })).toEqual('block block_modName_0') 215 | }) 216 | }) 217 | }) 218 | 219 | describe('withNaming custom preset', () => { 220 | const customCn = withNaming({ 221 | e: '__', 222 | m: '--', 223 | v: '_', 224 | }) 225 | 226 | test('variants', () => { 227 | const block = customCn('block') 228 | 229 | expect(block({ mod: true })).toEqual('block block--mod') 230 | expect(block({ mod: false })).toEqual('block') 231 | expect(block({ mod: 'value' })).toEqual('block block--mod_value') 232 | expect(block('element', { mod: true })).toEqual('block__element block__element--mod') 233 | expect(block('element', { mod: false })).toEqual('block__element') 234 | expect(block('element', { mod: 'value' })).toEqual('block__element block__element--mod_value') 235 | }) 236 | }) 237 | 238 | describe('carry', () => { 239 | test('alone', () => { 240 | const e = cn('Block') 241 | expect(e('Elem')).toEqual('Block-Elem') 242 | }) 243 | 244 | test('with mods', () => { 245 | const e = cn('Block') 246 | expect(e('Elem', { modName: true })).toEqual('Block-Elem Block-Elem_modName') 247 | }) 248 | 249 | test('with elemMods', () => { 250 | const e = cn('Block', 'Elem') 251 | expect(e({ modName: true })).toEqual('Block-Elem Block-Elem_modName') 252 | }) 253 | }) 254 | }) 255 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # @bem-react/core · [![npm (scoped)](https://img.shields.io/npm/v/@bem-react/core.svg)](https://www.npmjs.com/package/@bem-react/core) [![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/@bem-react/core.svg)](https://bundlephobia.com/result?p=@bem-react/core) 2 | 3 | Core package helps organize and manage components with [BEM modifiers](https://en.bem.info/methodology/key-concepts/#modifier) in React. 4 | 5 | ## Install 6 | 7 | ``` 8 | npm i -S @bem-react/core 9 | ``` 10 | 11 | ## Usage 12 | 13 | Let's say, you have an initial App file structure as follows: 14 | 15 | ``` 16 | App.tsx 17 | Components/ 18 | Button/ 19 | Button.tsx 20 | ``` 21 | 22 | And you need to set up two optional types of buttons that will be different from the `Button.tsx`. _(In our example those will be Button of theme 'action' and Button of type 'link')_ 23 | 24 | You can handle those using _@bem-react/core_. 25 | 26 | Follow the guide. 27 | 28 | #### Step 1. 29 | 30 | In your `Components/Button/index.tsx`, you define the type of props your button can get within the interface that extends **IClassNameProps** from '@bem-react/core' : 31 | 32 | ```ts 33 | import { ReactType } from 'react' 34 | import { IClassNameProps } from '@bem-react/core' 35 | import { cn } from '@bem-react/classname' 36 | 37 | export interface IButtonProps extends IClassNameProps { 38 | as?: ReactType 39 | } 40 | 41 | export const cnButton = cn('Button') 42 | ``` 43 | 44 | #### Step 2. 45 | 46 | Set up the **basic Button** variant which will be rendered if **no modifiers** props are set in the parent component. 47 | Inside your `Components/Button/Button.tsx`: 48 | 49 | ```tsx 50 | import React, { FC } from 'react' 51 | 52 | import { IButtonProps, cnButton } from './index' 53 | 54 | export const Button: FC = ({ 55 | children, 56 | className, 57 | as: Component = 'button', 58 | ...props 59 | }) => ( 60 | 61 | {children} 62 | 63 | ) 64 | ``` 65 | 66 | #### Step 3. 67 | 68 | Set up the **optional withButtonTypeLink** and **optional withButtonThemeAction** variants that will be rendered if `{type: 'link'}` and/or `{theme: 'action'}` modifiers are set in the parent component respectively. 69 | Inside your `Components/Button/` you add folders `_type/` with `Button_type_link.tsx` file in it and `_theme/` with `Button_theme_action.tsx` . 70 | 71 | ``` 72 | App.tsx 73 | Components/ 74 | Button/ 75 | Button.tsx 76 | index.tsx 77 | + _type/ 78 | + Button_type_link.tsx 79 | + _theme/ 80 | + Button_theme_action.tsx 81 | ``` 82 | 83 | Set up the variants: 84 | 85 | **Note!** The second parameter in `withBemMod()` is the condition for this component to be applied. 86 | 87 | **1.** In `Components/Button/_type/Button_type_link.tsx` 88 | 89 | ```tsx 90 | import React from 'react' 91 | import { withBemMod } from '@bem-react/core' 92 | 93 | import { IButtonProps, cnButton } from '../index' 94 | 95 | export interface IButtonTypeLinkProps { 96 | type?: 'link' 97 | href?: string 98 | } 99 | 100 | export const withButtonTypeLink = withBemMod( 101 | cnButton(), 102 | { type: 'link' }, 103 | (Button) => (props) => 149 | // Renders into HTML as: 150 | 151 | 152 | // Renders into HTML as: I'm type link 153 | 154 | 155 | // Renders into HTML as: 156 | 157 | 158 | // Renders into HTML as: I'm all together 159 |
160 | ); 161 | ``` 162 | 163 | **Note!** The order of optional components composed onto ButtonPresenter is important: in case you have different layouts and need to apply several modifiers the **FIRST** one inside the compose method will be rendered! 164 | E.g., here: 165 | 166 | ```tsx 167 | export const Button = compose( 168 | withButtonThemeAction, 169 | withButtonTypeLink, 170 | )(ButtonPresenter) 171 | ``` 172 | 173 | If your withButtonThemeAction was somewhat like 174 | 175 | `` 176 | 177 | your JSX-component: 178 | 179 | `` 180 | 181 | would render into HTML: 182 | 183 | `Hello` 184 | 185 | ## Use reexports for better DX 186 | 187 | > **IMPORTANT:** use this solution if [tree shaking](https://webpack.js.org/guides/tree-shaking/) enabled 188 | 189 | Example: 190 | 191 | ``` 192 | Block/Block.tsx 193 | Block/Block@desktop.tsx 194 | Block/_mod/Block_mod_val1.tsx 195 | Block/_mod/Block_mod_val2.tsx 196 | Block/_mod/Block_mod_val3.tsx 197 | ``` 198 | 199 | Create reexports for all modifiers in index files by platform: desktop, phone, amp, etc. 200 | 201 | ```ts 202 | // Block/index.ts 203 | export * from './Block' 204 | export * from './Block/_mod' 205 | 206 | // Block/desktop.ts 207 | export * from './Block@desktop' 208 | export * from './Block/_mod' 209 | 210 | // Block/phone.ts 211 | export * from './' // for feature if not created platform version 212 | 213 | // Block/_mod/index.ts 214 | export * from './Block_mod_val1.tsx' 215 | export * from './Block_mod_val2.tsx' 216 | export * from './Block_mod_val3.tsx' 217 | ``` 218 | 219 | Usage: 220 | 221 | ```ts 222 | // App.tsx 223 | import { Block as BlockPresenter, withModVal1 } from './components/Block/desktop' 224 | 225 | const Block = withModVal1(BlockPresenter) 226 | ``` 227 | 228 | ## Optimization. Lazy load for modifiers. 229 | 230 | Solution for better code splitting with React.lazy and dynamic imports 231 | 232 | > **NOTE** If your need SSR replace React.lazy method for load `Block_mod.async.tsx` module to [@loadable/components](https://www.smooth-code.com/open-source/loadable-components/) or [react-loadable](https://github.com/jamiebuilds/react-loadable) 233 | 234 | ```tsx 235 | // Block/_mod/Block_mod.async.tsx 236 | import React from 'react' 237 | import { cnBlock } from '../Block' 238 | 239 | import './Block_mod.css' 240 | 241 | export const DynamicPart: React.FC = () => Loaded dynamicly 242 | 243 | // default export needed for React.lazy 244 | export default DynamicPart 245 | ``` 246 | 247 | ```tsx 248 | // Block/_mod/Block_mod.tsx 249 | import React, { Suspense, lazy } from 'react' 250 | import { cnBlock } from '../Block' 251 | 252 | export interface BlockModProps { 253 | mod?: boolean 254 | } 255 | 256 | export const withMod = withBemMod(cnBlock(), { mod: true }, (Block) => (props) => { 257 | const DynamicPart = lazy(() => import('./Block_mod.async.tsx')) 258 | 259 | return ( 260 | Updating...
}> 261 | 262 | 263 | 264 | 265 | ) 266 | }) 267 | ``` 268 | 269 | Usage: 270 | 271 | ```ts 272 | // App.tsx 273 | import { 274 | Block as BlockPresenter, 275 | withMod 276 | } from './components/Block/desktop'; 277 | 278 | const Block = withMod(BlockPresenter); 279 | 280 | export const App = () => { 281 | return ( 282 | {/* chunk with DynamicPart not loaded */} 283 | 284 | 285 | {/* chunk with DynamicPart loaded */} 286 | 287 | ); 288 | } 289 | ``` 290 | 291 | ## Simple modifiers (only CSS classes) 292 | 293 | In most cases you need change only CSS class. This mode doesn't pass modififer value to props. 294 | It doesn't create React wrappers for component, that's way more optimal. 295 | 296 | ```tsx 297 | import React from 'react' 298 | import { cnBlock } from '../Block' 299 | 300 | export interface BlockModProps { 301 | simplemod?: boolean 302 | } 303 | 304 | export const withSimpleMod = createClassNameModifier(cnBlock(), { simplemod: true }) 305 | ``` 306 | 307 | ## Debug 308 | 309 | To help your debug "@bem-react/core" support development mode. 310 | 311 | For `` (from the **Example** above), React DevTools will show: 312 | 313 | ```html 314 | 315 | 316 | Hello 317 | 318 | 319 | ``` 320 | -------------------------------------------------------------------------------- /packages/di/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [5.1.0](https://github.com/bem/bem-react/compare/@bem-react/di@5.0.0...@bem-react/di@5.1.0) (2025-06-15) 7 | 8 | ### Features 9 | 10 | - add dual packaging ([6d78a65](https://github.com/bem/bem-react/commit/6d78a651214abb057390338e4ea249c264c3252d)) 11 | - support esm modules ([7ef51f8](https://github.com/bem/bem-react/commit/7ef51f8a96d1b81f60458a4772237813eb630c42)) 12 | 13 | # [5.0.0](https://github.com/bem/bem-react/compare/@bem-react/di@3.1.1...@bem-react/di@5.0.0) (2022-12-15) 14 | 15 | ### Bug Fixes 16 | 17 | - types ([17ab34b](https://github.com/bem/bem-react/commit/17ab34b168c5f5904b6a9b87c0fbb7f9a071cf0a)) 18 | 19 | ### Features 20 | 21 | - update to react@18 ([f08e4d6](https://github.com/bem/bem-react/commit/f08e4d686d7891e4356859932ee18812700a4e27)) 22 | 23 | # [4.0.0](https://github.com/bem/bem-react/compare/@bem-react/di@3.1.1...@bem-react/di@4.0.0) (2022-12-15) 24 | 25 | ### Bug Fixes 26 | 27 | - types ([17ab34b](https://github.com/bem/bem-react/commit/17ab34b168c5f5904b6a9b87c0fbb7f9a071cf0a)) 28 | 29 | ### Features 30 | 31 | - update to react@18 ([f08e4d6](https://github.com/bem/bem-react/commit/f08e4d686d7891e4356859932ee18812700a4e27)) 32 | 33 | ## [3.1.1](https://github.com/bem/bem-react/compare/@bem-react/di@3.1.0...@bem-react/di@3.1.1) (2021-06-29) 34 | 35 | ### Bug Fixes 36 | 37 | - **di:** fix partial merge of registries ([3ef511d](https://github.com/bem/bem-react/commit/3ef511dc18b01676d9528b62e71b81c9ca16d71d)) 38 | 39 | # [3.1.0](https://github.com/bem/bem-react/compare/@bem-react/di@3.0.0...@bem-react/di@3.1.0) (2021-06-23) 40 | 41 | ### Features 42 | 43 | - **di:** rename ComponentRegistryConsumer to RegistryConsumer ([2420041](https://github.com/bem/bem-react/commit/24200415e8b54868ab7932a9531e5313d316b526)) 44 | 45 | # [3.0.0](https://github.com/bem/bem-react/compare/@bem-react/di@2.2.8...@bem-react/di@3.0.0) (2021-06-22) 46 | 47 | ### Features 48 | 49 | - **di:** make di able to keep anything ([e302953](https://github.com/bem/bem-react/commit/e30295305e133ba240e5dc691eb80ab04199c12e)) 50 | 51 | ### BREAKING CHANGES 52 | 53 | - **di:** `HOC` and `IRegistryComponents` aren't exported from di, and generic-param for `Registry.set` has different meaning 54 | 55 | ## [2.2.8](https://github.com/bem/bem-react/compare/@bem-react/di@2.2.7...@bem-react/di@2.2.8) (2021-06-21) 56 | 57 | ### Bug Fixes 58 | 59 | - **di:** expose IRegistryComponents type ([d32a0a7](https://github.com/bem/bem-react/commit/d32a0a7e24f5082943a7b9d854f00a17437de8fe)) 60 | 61 | ## [2.2.7](https://github.com/bem/bem-react/compare/@bem-react/di@2.2.6...@bem-react/di@2.2.7) (2021-06-08) 62 | 63 | ### Bug Fixes 64 | 65 | - update pkg ([1ccdee8](https://github.com/bem/bem-react/commit/1ccdee8d9c4c09a02f888ee880a332ac75b725fd)) 66 | 67 | ## [2.2.6](https://github.com/bem/bem-react/compare/@bem-react/di@2.2.5...@bem-react/di@2.2.6) (2021-04-27) 68 | 69 | ### Bug Fixes 70 | 71 | - **di:** supports react@17 ([0c19c61](https://github.com/bem/bem-react/commit/0c19c6135a0fdbbf82dcb808f745b57320dbad76)) 72 | 73 | ## [2.2.5](https://github.com/bem/bem-react/compare/@bem-react/di@2.2.4...@bem-react/di@2.2.5) (2020-11-13) 74 | 75 | ### Bug Fixes 76 | 77 | - **di:** fixed merge of registries with same id ([0706c4a](https://github.com/bem/bem-react/commit/0706c4ad5117c3107df24d42abe8b67eebbec30c)) 78 | 79 | ## [2.2.4](https://github.com/bem/bem-react/compare/@bem-react/di@2.2.3...@bem-react/di@2.2.4) (2020-04-02) 80 | 81 | ### Bug Fixes 82 | 83 | - **di:** resolve [#551](https://github.com/bem/bem-react/issues/551) issue ([c4491a4](https://github.com/bem/bem-react/commit/c4491a44268bd61ec77316208b918c03abea65c8)) 84 | 85 | ## [2.2.3](https://github.com/bem/bem-react/compare/@bem-react/di@2.2.2...@bem-react/di@2.2.3) (2020-03-12) 86 | 87 | ### Performance Improvements 88 | 89 | - **di:** use for of loop instead forEach ([68d239c](https://github.com/bem/bem-react/commit/68d239c3f537a7203a9d8644a81ab4623fedb2eb)) 90 | - **di:** use native createElement instead jsx ([eb3ff64](https://github.com/bem/bem-react/commit/eb3ff6461a1eaa0558df0ab3aebf32a302a35a77)) 91 | 92 | ## [2.2.2](https://github.com/bem/bem-react/compare/@bem-react/di@2.2.1...@bem-react/di@2.2.2) (2020-03-02) 93 | 94 | ### Bug Fixes 95 | 96 | - **di:** fixes a way to extends components ([629b2a5](https://github.com/bem/bem-react/commit/629b2a508d92997b30a2f7a9342b2f0fd4337e4b)) 97 | 98 | ### Performance Improvements 99 | 100 | - **di:** improving performance ([469966f](https://github.com/bem/bem-react/commit/469966f01f4c11f27971625d8681f38beabcd773)) 101 | 102 | ## [2.2.1](https://github.com/bem/bem-react/compare/@bem-react/di@2.2.0...@bem-react/di@2.2.1) (2019-12-27) 103 | 104 | ### Bug Fixes 105 | 106 | - **di:** Add errors when base component is empty for extending component ([8595d01](https://github.com/bem/bem-react/commit/8595d01194ae00af4216a5d5824205c62d1e1161)) 107 | 108 | # [2.2.0](https://github.com/bem/bem-react/compare/@bem-react/di@2.1.0...@bem-react/di@2.2.0) (2019-12-23) 109 | 110 | ### Features 111 | 112 | - **di:** Add withBase-hoc as a way of extending components ([dda4d8b](https://github.com/bem/bem-react/commit/dda4d8b22325331a46e19dad75dae7da5a388aed)) 113 | 114 | # [2.1.0](https://github.com/bem/bem-react/compare/@bem-react/di@2.0.4...@bem-react/di@2.1.0) (2019-12-02) 115 | 116 | ### Features 117 | 118 | - **di:** fill registry with components via object literal ([a4f69c5](https://github.com/bem/bem-react/commit/a4f69c5c12e4bdf31c66994174d75fd82bc76674)) 119 | 120 | ## [2.0.4](https://github.com/bem/bem-react/compare/@bem-react/di@2.0.3...@bem-react/di@2.0.4) (2019-10-02) 121 | 122 | **Note:** Version bump only for package @bem-react/di 123 | 124 | ## [2.0.3](https://github.com/bem/bem-react/compare/@bem-react/di@2.0.2...@bem-react/di@2.0.3) (2019-08-20) 125 | 126 | **Note:** Version bump only for package @bem-react/di 127 | 128 | ## [2.0.2](https://github.com/bem/bem-react/compare/@bem-react/di@2.0.1...@bem-react/di@2.0.2) (2019-07-31) 129 | 130 | **Note:** Version bump only for package @bem-react/di 131 | 132 | ## [2.0.1](https://github.com/bem/bem-react/compare/@bem-react/di@2.0.0...@bem-react/di@2.0.1) (2019-05-27) 133 | 134 | ### Bug Fixes 135 | 136 | - **di:** return type in GetNonDefaultProps without GetNonDefaultProps ([9f3ab8e](https://github.com/bem/bem-react/commit/9f3ab8e)) 137 | 138 | # [2.0.0](https://github.com/bem/bem-react/tree/master/packages/di/compare/@bem-react/di@1.6.0...@bem-react/di@2.0.0) (2019-05-24) 139 | 140 | ### Features 141 | 142 | - **di:** replace inverted by overridable ([957a0fe](https://github.com/bem/bem-react/commit/957a0fe)) 143 | 144 | ### BREAKING CHANGES 145 | 146 | - **di:** Set inverted flag by default and rename it to "overridable". 147 | 148 | # [1.6.0](https://github.com/bem/bem-react/tree/master/packages/di/compare/@bem-react/di@1.5.3...@bem-react/di@1.6.0) (2019-04-22) 149 | 150 | ### Features 151 | 152 | - **di:** add hooks for registries and registryComponent ([c512dc2](https://github.com/bem/bem-react/commit/c512dc2)) 153 | 154 | ## [1.5.3](https://github.com/bem/bem-react/tree/master/packages/di/compare/@bem-react/di@1.5.2...@bem-react/di@1.5.3) (2019-03-01) 155 | 156 | ### Bug Fixes 157 | 158 | - **di:** registers are overwritten in context ([a7b6377](https://github.com/bem/bem-react/commit/a7b6377)) 159 | 160 | ## [1.5.2](https://github.com/bem/bem-react/tree/master/packages/di/compare/@bem-react/di@1.5.1...@bem-react/di@1.5.2) (2019-01-29) 161 | 162 | ### Bug Fixes 163 | 164 | - **di:** remove global variable providedRegistries ([8f5e93e](https://github.com/bem/bem-react/commit/8f5e93e)) 165 | 166 | ## [1.5.1](https://github.com/bem/bem-react/tree/master/packages/di/compare/@bem-react/di@1.5.0...@bem-react/di@1.5.1) (2019-01-16) 167 | 168 | ### Bug Fixes 169 | 170 | - **di:** provided registries must be global ([57fdb8b](https://github.com/bem/bem-react/commit/57fdb8b)) 171 | 172 | # [1.5.0](https://github.com/bem/bem-react/tree/master/packages/di/compare/@bem-react/di@1.4.0...@bem-react/di@1.5.0) (2019-01-10) 173 | 174 | ### Features 175 | 176 | - **di:** partially registries merge ([7890e03](https://github.com/bem/bem-react/commit/7890e03)) 177 | 178 | # [1.4.0](https://github.com/bem/bem-react/tree/master/packages/di/compare/@bem-react/di@1.3.0...@bem-react/di@1.4.0) (2018-12-28) 179 | 180 | ### Features 181 | 182 | - **di:** the way to add typings for registry result ([b76e4e1](https://github.com/bem/bem-react/commit/b76e4e1)) 183 | 184 | # 1.3.0 (2018-12-21) 185 | 186 | ### Bug Fixes 187 | 188 | - **di:** correct typings for withRegistry ([a79eca2](https://github.com/bem/bem-react/commit/a79eca2)) 189 | - **di:** return correct type from withRegistry ([e695088](https://github.com/bem/bem-react/commit/e695088)) 190 | - **di:** use map as class option for using in es5 ([24e9015](https://github.com/bem/bem-react/commit/24e9015)) 191 | 192 | ### Features 193 | 194 | - **v3:** init packages ([d652328](https://github.com/bem/bem-react/commit/d652328)) 195 | 196 | ## 1.2.2 (2018-12-21) 197 | 198 | ### Bug Fixes 199 | 200 | - **di:** correct typings for withRegistry ([a79eca2](https://github.com/bem/bem-react/commit/a79eca2)) 201 | - **di:** return correct type from withRegistry ([e695088](https://github.com/bem/bem-react/commit/e695088)) 202 | - **di:** use map as class option for using in es5 ([24e9015](https://github.com/bem/bem-react/commit/24e9015)) 203 | 204 | ### Features 205 | 206 | - **v3:** init packages ([d652328](https://github.com/bem/bem-react/commit/d652328)) 207 | 208 | ## 1.2.1 (2018-12-19) 209 | 210 | ### Bug Fixes 211 | 212 | - **di:** correct typings for withRegistry ([a79eca2](https://github.com/bem/bem-react/commit/a79eca2)) 213 | - **di:** return correct type from withRegistry ([4e09616](https://github.com/bem/bem-react/commit/4e09616)) 214 | - **di:** use map as class option for using in es5 ([24e9015](https://github.com/bem/bem-react/commit/24e9015)) 215 | 216 | ### Features 217 | 218 | - **v3:** init packages ([d652328](https://github.com/bem/bem-react/commit/d652328)) 219 | 220 | # 1.2.0 (2018-12-18) 221 | 222 | ### Bug Fixes 223 | 224 | - **di:** correct typings for withRegistry ([ce73d79](https://github.com/bem/bem-react/commit/ce73d79)) 225 | - **di:** use map as class option for using in es5 ([24e9015](https://github.com/bem/bem-react/commit/24e9015)) 226 | 227 | ### Features 228 | 229 | - **v3:** init packages ([d652328](https://github.com/bem/bem-react/commit/d652328)) 230 | 231 | # 1.1.0 (2018-12-06) 232 | 233 | ### Bug Fixes 234 | 235 | - **di:** use map as class option for using in es5 ([24e9015](https://github.com/bem/bem-react/commit/24e9015)) 236 | 237 | ### Features 238 | 239 | - **v3:** init packages ([d652328](https://github.com/bem/bem-react/commit/d652328)) 240 | 241 | 242 | 243 | ## [1.0.2](https://github.com/bem/bem-react-core/compare/@bem-react/di@1.0.1...@bem-react/di@1.0.2) (2018-10-24) 244 | 245 | **Note:** Version bump only for package @bem-react/di 246 | 247 | 248 | 249 | ## [1.0.1](https://github.com/bem/bem-react-core/compare/@bem-react/di@0.2.4...@bem-react/di@1.0.1) (2018-10-02) 250 | 251 | ### Bug Fixes 252 | 253 | - **di:** use map as class option for using in es5 ([7fd489f](https://github.com/bem/bem-react-core/commit/7fd489f)) 254 | 255 | 256 | 257 | # [1.0.0](https://github.com/bem/bem-react-core/compare/@bem-react/di@0.2.4...@bem-react/di@1.0.0) (2018-09-20) 258 | 259 | **Note:** Version bump only for package @bem-react/di 260 | 261 | 262 | 263 | ## [0.2.4](https://github.com/bem/bem-react-core/compare/@bem-react/di@0.2.3...@bem-react/di@0.2.4) (2018-09-13) 264 | 265 | **Note:** Version bump only for package @bem-react/di 266 | 267 | 268 | 269 | ## [0.2.3](https://github.com/bem/bem-react-core/compare/@bem-react/di@0.2.2...@bem-react/di@0.2.3) (2018-08-30) 270 | 271 | **Note:** Version bump only for package @bem-react/di 272 | 273 | 274 | 275 | ## [0.2.2](https://github.com/bem/bem-react-core/compare/@bem-react/di@0.2.1...@bem-react/di@0.2.2) (2018-08-29) 276 | 277 | **Note:** Version bump only for package @bem-react/di 278 | 279 | 280 | 281 | ## [0.2.1](https://github.com/bem/bem-react-core/compare/@bem-react/di@0.2.0...@bem-react/di@0.2.1) (2018-08-29) 282 | 283 | **Note:** Version bump only for package @bem-react/di 284 | 285 | 286 | 287 | # 0.2.0 (2018-08-29) 288 | 289 | ### Features 290 | 291 | - **v3:** init packages ([b192fc5](https://github.com/bem/bem-react-core/commit/b192fc5)) 292 | 293 | 294 | 295 | # 0.1.0 (2018-08-29) 296 | 297 | ### Features 298 | 299 | - **v3:** init packages ([b192fc5](https://github.com/bem/bem-react-core/commit/b192fc5)) 300 | -------------------------------------------------------------------------------- /packages/classname/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [1.7.0](https://github.com/bem/bem-react/compare/@bem-react/classname@1.6.0...@bem-react/classname@1.7.0) (2025-06-15) 7 | 8 | ### Features 9 | 10 | - add dual packaging ([6d78a65](https://github.com/bem/bem-react/commit/6d78a651214abb057390338e4ea249c264c3252d)) 11 | - support esm modules ([7ef51f8](https://github.com/bem/bem-react/commit/7ef51f8a96d1b81f60458a4772237813eb630c42)) 12 | 13 | # [1.6.0](https://github.com/bem/bem-react/compare/@bem-react/classname@1.5.12...@bem-react/classname@1.6.0) (2023-04-04) 14 | 15 | ### Features 16 | 17 | - allow mix to be a string ([c1c9731](https://github.com/bem/bem-react/commit/c1c97312b80d24da88eafffd8e7c235887d65bd6)) 18 | 19 | ## [1.5.12](https://github.com/bem/bem-react/compare/@bem-react/classname@1.5.11...@bem-react/classname@1.5.12) (2021-09-03) 20 | 21 | ### Bug Fixes 22 | 23 | - **classname:** add correct overloads for cn formatter ([47758e7](https://github.com/bem/bem-react/commit/47758e76847e34babddbb99b60fd3f9827f49e95)) 24 | 25 | ## [1.5.11](https://github.com/bem/bem-react/compare/@bem-react/classname@1.5.10...@bem-react/classname@1.5.11) (2021-06-08) 26 | 27 | ### Bug Fixes 28 | 29 | - update pkg ([1ccdee8](https://github.com/bem/bem-react/commit/1ccdee8d9c4c09a02f888ee880a332ac75b725fd)) 30 | 31 | ## [1.5.10](https://github.com/bem/bem-react/compare/@bem-react/classname@1.5.9...@bem-react/classname@1.5.10) (2021-04-29) 32 | 33 | ### Bug Fixes 34 | 35 | - **classname:** formatter type ([04e8b5c](https://github.com/bem/bem-react/commit/04e8b5c00724e3817b624acf108a7186e94af200)) 36 | 37 | ## [1.5.9](https://github.com/bem/bem-react/compare/@bem-react/classname@1.5.8...@bem-react/classname@1.5.9) (2021-02-11) 38 | 39 | ### Bug Fixes 40 | 41 | - **classname:** support mix via valueOf method ([243575c](https://github.com/bem/bem-react/commit/243575c918f7e157d45f2379ef5c31c16975b8ab)) 42 | 43 | ## [1.5.8](https://github.com/bem/bem-react/compare/@bem-react/classname@1.5.7...@bem-react/classname@1.5.8) (2020-03-12) 44 | 45 | **Note:** Version bump only for package @bem-react/classname 46 | 47 | ## [1.5.7](https://github.com/bem/bem-react/compare/@bem-react/classname@1.5.6...@bem-react/classname@1.5.7) (2020-03-02) 48 | 49 | **Note:** Version bump only for package @bem-react/classname 50 | 51 | ## [1.5.6](https://github.com/bem/bem-react/compare/@bem-react/classname@1.5.5...@bem-react/classname@1.5.6) (2019-10-02) 52 | 53 | **Note:** Version bump only for package @bem-react/classname 54 | 55 | ## [1.5.5](https://github.com/bem/bem-react/compare/@bem-react/classname@1.5.4...@bem-react/classname@1.5.5) (2019-10-02) 56 | 57 | **Note:** Version bump only for package @bem-react/classname 58 | 59 | ## [1.5.4](https://github.com/bem/bem-react/compare/@bem-react/classname@1.5.3...@bem-react/classname@1.5.4) (2019-08-20) 60 | 61 | **Note:** Version bump only for package @bem-react/classname 62 | 63 | ## [1.5.3](https://github.com/bem/bem-react/compare/@bem-react/classname@1.5.2...@bem-react/classname@1.5.3) (2019-07-31) 64 | 65 | ### Bug Fixes 66 | 67 | - **classname:** mix as not string type ([9648499](https://github.com/bem/bem-react/commit/9648499)) 68 | 69 | ## [1.5.2](https://github.com/bem/bem-react/compare/@bem-react/classname@1.5.1...@bem-react/classname@1.5.2) (2019-05-27) 70 | 71 | **Note:** Version bump only for package @bem-react/classname 72 | 73 | ## [1.5.1](https://github.com/bem/bem-react/tree/master/packages/classname/compare/@bem-react/classname@1.5.0...@bem-react/classname@1.5.1) (2019-05-24) 74 | 75 | **Note:** Version bump only for package @bem-react/classname 76 | 77 | # [1.5.0](https://github.com/bem/bem-react/tree/master/packages/classname/compare/@bem-react/classname@1.4.4...@bem-react/classname@1.5.0) (2019-04-22) 78 | 79 | ### Features 80 | 81 | - **classname:** add modVal delimiter for withNaming configuration ([5fb63dc](https://github.com/bem/bem-react/commit/5fb63dc)) 82 | 83 | ## [1.4.4](https://github.com/bem/bem-react/tree/master/packages/classname/compare/@bem-react/classname@1.4.3...@bem-react/classname@1.4.4) (2019-03-01) 84 | 85 | **Note:** Version bump only for package @bem-react/classname 86 | 87 | ## [1.4.3](https://github.com/bem/bem-react/tree/master/packages/classname/compare/@bem-react/classname@1.4.2...@bem-react/classname@1.4.3) (2019-01-17) 88 | 89 | ### Bug Fixes 90 | 91 | - **classname:** filter class name duplicates with mods ([1cfb22c](https://github.com/bem/bem-react/commit/1cfb22c)) 92 | 93 | ## [1.4.2](https://github.com/bem/bem-react/tree/master/packages/classname/compare/@bem-react/classname@1.4.1...@bem-react/classname@1.4.2) (2019-01-17) 94 | 95 | ### Bug Fixes 96 | 97 | - **classname:** filter class name duplicates ([e92cbab](https://github.com/bem/bem-react/commit/e92cbab)) 98 | 99 | ## [1.4.1](https://github.com/bem/bem-react/tree/master/packages/classname/compare/@bem-react/classname@1.4.0...@bem-react/classname@1.4.1) (2018-12-28) 100 | 101 | **Note:** Version bump only for package @bem-react/classname 102 | 103 | # 1.4.0 (2018-12-21) 104 | 105 | ### Bug Fixes 106 | 107 | - classname with undefined ([9232c61](https://github.com/bem/bem-react/commit/9232c61)) 108 | - fix filename ([3dbdcdd](https://github.com/bem/bem-react/commit/3dbdcdd)) 109 | - **classname:** remove undefined modifiers ([c3af486](https://github.com/bem/bem-react/commit/c3af486)) 110 | - **classname:** remove whitespace after mix ([5e423e7](https://github.com/bem/bem-react/commit/5e423e7)) 111 | - **classname:** use set for unique class list ([9a708b1](https://github.com/bem/bem-react/commit/9a708b1)) 112 | 113 | ### Features 114 | 115 | - **classname:** array type for mix ([9513c26](https://github.com/bem/bem-react/commit/9513c26)) 116 | - **classname:** carry elems ([a943509](https://github.com/bem/bem-react/commit/a943509)) 117 | - **classname:** decrease bundle size, classnames pkg ([c5fb74f](https://github.com/bem/bem-react/commit/c5fb74f)) 118 | - **v3:** init packages ([c70a97d](https://github.com/bem/bem-react/commit/c70a97d)) 119 | - **v3:** init packages ([d652328](https://github.com/bem/bem-react/commit/d652328)) 120 | 121 | ## 1.3.2 (2018-12-21) 122 | 123 | ### Bug Fixes 124 | 125 | - classname with undefined ([9232c61](https://github.com/bem/bem-react/commit/9232c61)) 126 | - fix filename ([3dbdcdd](https://github.com/bem/bem-react/commit/3dbdcdd)) 127 | - **classname:** remove undefined modifiers ([6a595d9](https://github.com/bem/bem-react/commit/6a595d9)) 128 | - **classname:** remove whitespace after mix ([2a9f8f0](https://github.com/bem/bem-react/commit/2a9f8f0)) 129 | - **classname:** use set for unique class list ([9a708b1](https://github.com/bem/bem-react/commit/9a708b1)) 130 | 131 | ### Features 132 | 133 | - **classname:** array type for mix ([9513c26](https://github.com/bem/bem-react/commit/9513c26)) 134 | - **classname:** carry elems ([a943509](https://github.com/bem/bem-react/commit/a943509)) 135 | - **classname:** decrease bundle size, classnames pkg ([c5fb74f](https://github.com/bem/bem-react/commit/c5fb74f)) 136 | - **v3:** init packages ([c70a97d](https://github.com/bem/bem-react/commit/c70a97d)) 137 | - **v3:** init packages ([d652328](https://github.com/bem/bem-react/commit/d652328)) 138 | 139 | ## 1.3.1 (2018-12-19) 140 | 141 | ### Bug Fixes 142 | 143 | - **classname:** use set for unique class list ([9a708b1](https://github.com/bem/bem-react/commit/9a708b1)) 144 | - classname with undefined ([9232c61](https://github.com/bem/bem-react/commit/9232c61)) 145 | - fix filename ([3dbdcdd](https://github.com/bem/bem-react/commit/3dbdcdd)) 146 | 147 | ### Features 148 | 149 | - **classname:** array type for mix ([9513c26](https://github.com/bem/bem-react/commit/9513c26)) 150 | - **classname:** carry elems ([a943509](https://github.com/bem/bem-react/commit/a943509)) 151 | - **classname:** decrease bundle size, classnames pkg ([c5fb74f](https://github.com/bem/bem-react/commit/c5fb74f)) 152 | - **v3:** init packages ([c70a97d](https://github.com/bem/bem-react/commit/c70a97d)) 153 | - **v3:** init packages ([d652328](https://github.com/bem/bem-react/commit/d652328)) 154 | 155 | # 1.3.0 (2018-12-18) 156 | 157 | ### Bug Fixes 158 | 159 | - **classname:** use set for unique class list ([9a708b1](https://github.com/bem/bem-react/commit/9a708b1)) 160 | - classname with undefined ([9232c61](https://github.com/bem/bem-react/commit/9232c61)) 161 | - fix filename ([3dbdcdd](https://github.com/bem/bem-react/commit/3dbdcdd)) 162 | 163 | ### Features 164 | 165 | - **classname:** array type for mix ([9513c26](https://github.com/bem/bem-react/commit/9513c26)) 166 | - **classname:** carry elems ([a943509](https://github.com/bem/bem-react/commit/a943509)) 167 | - **classname:** decrease bundle size, classnames pkg ([c5fb74f](https://github.com/bem/bem-react/commit/c5fb74f)) 168 | - **v3:** init packages ([c70a97d](https://github.com/bem/bem-react/commit/c70a97d)) 169 | - **v3:** init packages ([d652328](https://github.com/bem/bem-react/commit/d652328)) 170 | 171 | # 1.2.0 (2018-12-06) 172 | 173 | ### Bug Fixes 174 | 175 | - **classname:** use set for unique class list ([9a708b1](https://github.com/bem/bem-react/commit/9a708b1)) 176 | - classname with undefined ([9232c61](https://github.com/bem/bem-react/commit/9232c61)) 177 | - fix filename ([3dbdcdd](https://github.com/bem/bem-react/commit/3dbdcdd)) 178 | 179 | ### Features 180 | 181 | - **classname:** array type for mix ([9513c26](https://github.com/bem/bem-react/commit/9513c26)) 182 | - **classname:** carry elems ([a943509](https://github.com/bem/bem-react/commit/a943509)) 183 | - **classname:** decrease bundle size, classnames pkg ([c5fb74f](https://github.com/bem/bem-react/commit/c5fb74f)) 184 | - **v3:** init packages ([c70a97d](https://github.com/bem/bem-react/commit/c70a97d)) 185 | - **v3:** init packages ([d652328](https://github.com/bem/bem-react/commit/d652328)) 186 | 187 | 188 | 189 | ## [1.1.1](https://github.com/bem/bem-react-core/compare/@bem-react/classname@1.1.0...@bem-react/classname@1.1.1) (2018-10-24) 190 | 191 | ### Bug Fixes 192 | 193 | - **classname:** use set for unique class list ([6f4aa5f](https://github.com/bem/bem-react-core/commit/6f4aa5f)) 194 | - classname with undefined ([5f0e907](https://github.com/bem/bem-react-core/commit/5f0e907)) 195 | 196 | 197 | 198 | # [1.1.0](https://github.com/bem/bem-react-core/compare/@bem-react/classname@0.3.2...@bem-react/classname@1.1.0) (2018-10-02) 199 | 200 | ### Features 201 | 202 | - **classname:** array type for mix ([dd985e8](https://github.com/bem/bem-react-core/commit/dd985e8)) 203 | 204 | 205 | 206 | # [1.0.0](https://github.com/bem/bem-react-core/compare/@bem-react/classname@0.3.2...@bem-react/classname@1.0.0) (2018-09-20) 207 | 208 | **Note:** Version bump only for package @bem-react/classname 209 | 210 | 211 | 212 | ## [0.3.2](https://github.com/bem/bem-react-core/compare/@bem-react/classname@0.3.1...@bem-react/classname@0.3.2) (2018-09-04) 213 | 214 | **Note:** Version bump only for package @bem-react/classname 215 | 216 | 217 | 218 | ## [0.3.1](https://github.com/bem/bem-react-core/compare/@bem-react/classname@0.3.0...@bem-react/classname@0.3.1) (2018-08-30) 219 | 220 | **Note:** Version bump only for package @bem-react/classname 221 | 222 | 223 | 224 | # [0.3.0](https://github.com/bem/bem-react-core/compare/@bem-react/classname@0.2.2...@bem-react/classname@0.3.0) (2018-08-30) 225 | 226 | ### Features 227 | 228 | - **classname:** carry elems ([81f28c3](https://github.com/bem/bem-react-core/commit/81f28c3)) 229 | 230 | 231 | 232 | ## [0.2.2](https://github.com/bem/bem-react-core/compare/@bem-react/classname@0.2.1...@bem-react/classname@0.2.2) (2018-08-30) 233 | 234 | **Note:** Version bump only for package @bem-react/classname 235 | 236 | 237 | 238 | ## [0.2.1](https://github.com/bem/bem-react-core/compare/@bem-react/classname@0.2.0...@bem-react/classname@0.2.1) (2018-08-29) 239 | 240 | **Note:** Version bump only for package @bem-react/classname 241 | 242 | 243 | 244 | # 0.2.0 (2018-08-29) 245 | 246 | ### Bug Fixes 247 | 248 | - fix filename ([ee0f862](https://github.com/bem/bem-react-core/commit/ee0f862)) 249 | 250 | ### Features 251 | 252 | - **v3:** init packages ([00423c8](https://github.com/bem/bem-react-core/commit/00423c8)) 253 | - **v3:** init packages ([b192fc5](https://github.com/bem/bem-react-core/commit/b192fc5)) 254 | 255 | 256 | 257 | # 0.1.0 (2018-08-29) 258 | 259 | ### Bug Fixes 260 | 261 | - fix filename ([ee0f862](https://github.com/bem/bem-react-core/commit/ee0f862)) 262 | 263 | ### Features 264 | 265 | - **v3:** init packages ([00423c8](https://github.com/bem/bem-react-core/commit/00423c8)) 266 | - **v3:** init packages ([b192fc5](https://github.com/bem/bem-react-core/commit/b192fc5)) 267 | -------------------------------------------------------------------------------- /packages/di/test/di.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react' 2 | import { render } from '@testing-library/react' 3 | 4 | import { Registry, withRegistry, RegistryConsumer, useRegistries, useRegistry } from '../di' 5 | 6 | function expectText(Component: React.ReactElement, text: string) { 7 | expect(render(Component).container.textContent).toEqual(text) 8 | } 9 | 10 | type BaseProps = { 11 | children?: ReactNode 12 | } 13 | 14 | describe('@bem-react/di', () => { 15 | describe('Registry', () => { 16 | test('should set components by id', () => { 17 | const registry = new Registry({ id: 'registry' }) 18 | const Component1 = () => null 19 | const Component2 = () => 20 | 21 | registry.set('id-1', Component1).set('id-2', Component2) 22 | 23 | expect(registry.get('id-1')).toEqual(Component1) 24 | expect(registry.get('id-2')).toEqual(Component2) 25 | }) 26 | 27 | test('should fill components via object literal', () => { 28 | const registry = new Registry({ id: 'registry' }) 29 | const Component1 = () => null 30 | const Component2 = () => 31 | 32 | registry.fill({ 33 | Component1, 34 | Component2, 35 | }) 36 | 37 | const snapshot: any = {} 38 | snapshot.Component1 = Component1 39 | snapshot.Component2 = Component2 40 | 41 | expect(registry.snapshot()).toEqual(snapshot) 42 | }) 43 | 44 | test('should return list of components', () => { 45 | const registry = new Registry({ id: 'registry' }) 46 | const Component1 = () => null 47 | const Component2 = () => 48 | 49 | registry.set('id-1', Component1).set('id-2', Component2) 50 | 51 | const snapshot = { 52 | 'id-1': Component1, 53 | 'id-2': Component2, 54 | } 55 | 56 | expect(registry.snapshot()).toEqual(snapshot) 57 | }) 58 | 59 | test('should add/retrieve other values', () => { 60 | const registry = new Registry({ id: 'registry' }) 61 | const functor = () => 'value' 62 | 63 | registry.set('id-1', 1).fill({ 'id-2': '2-string', 'id-3': functor }) 64 | 65 | expect(registry.snapshot()).toEqual({ 66 | 'id-1': 1, 67 | 'id-2': '2-string', 68 | 'id-3': functor, 69 | }) 70 | }) 71 | 72 | test('should merge registries', () => { 73 | const baseRegistry = new Registry({ id: 'base' }) 74 | const Component1 = () => null 75 | const Component2 = () => 76 | 77 | baseRegistry.fill({ Component1, Component2 }) 78 | 79 | const overrideRegistry = new Registry({ id: 'override' }) 80 | const Component1Overwrite = () =>
81 | 82 | overrideRegistry.fill({ Component1: Component1Overwrite }) 83 | 84 | const snapshot = { 85 | Component1: Component1Overwrite, 86 | Component2, 87 | } 88 | 89 | expect(baseRegistry.merge(overrideRegistry).snapshot()).toEqual(snapshot) 90 | }) 91 | 92 | test('should not affect registry in merge with undefined', () => { 93 | const registry = new Registry({ id: 'registry' }) 94 | const Component1 = () => null 95 | const Component2 = () => 96 | 97 | registry.set('id-1', Component1).set('id-2', Component2) 98 | 99 | const snapshot = { 100 | 'id-1': Component1, 101 | 'id-2': Component2, 102 | } 103 | 104 | // @ts-ignore to check inside logic 105 | expect(registry.merge().snapshot()).toEqual(snapshot) 106 | }) 107 | 108 | test("should throw error when component doesn't exist", () => { 109 | const registry = new Registry({ id: 'registry' }) 110 | 111 | expect(() => registry.get('id')).toThrow("Entry with id 'id' not found.") 112 | }) 113 | }) 114 | 115 | describe('withRegistry', () => { 116 | describe('useRegistry', () => { 117 | test('should pull component from registry', () => { 118 | const registry = new Registry({ id: 'registry' }) 119 | const Element: React.FC = ({ children }) => {children} 120 | 121 | registry.fill({ Element }) 122 | 123 | const AppPresenter: React.FC = ({ children }) => { 124 | const { Element } = useRegistry('registry') as { Element: React.FC } 125 | 126 | return {children} 127 | } 128 | 129 | const App = withRegistry(registry)(AppPresenter) 130 | 131 | expectText(, 'content') 132 | }) 133 | }) 134 | 135 | describe('useRegistries', () => { 136 | test('should pull components from different registries', () => { 137 | const registry1 = new Registry({ id: 'registry1' }) 138 | const registry2 = new Registry({ id: 'registry2' }) 139 | const Element1 = () => content-1 140 | const Element2 = () => content-2 141 | 142 | registry1.fill({ Element1 }) 143 | registry2.fill({ Element2 }) 144 | 145 | const AppPresenter: React.FC = () => { 146 | const { registry1, registry2 } = useRegistries() 147 | const { Element1 } = registry1.snapshot() as { Element1: React.FC } 148 | const { Element2 } = registry2.snapshot() as { Element2: React.FC } 149 | 150 | return ( 151 | <> 152 | 153 | 154 | 155 | ) 156 | } 157 | 158 | const App = withRegistry(registry1, registry2)(AppPresenter) 159 | 160 | expectText(, 'content-1content-2') 161 | }) 162 | }) 163 | 164 | describe('RegistryConsumer', () => { 165 | test('should pull component from registry', () => { 166 | const registry = new Registry({ id: 'registry' }) 167 | const Element: React.FC = ({ children }) => {children} 168 | 169 | registry.fill({ Element }) 170 | 171 | const AppPresenter: React.FC = ({ children }) => ( 172 | 173 | {({ Element }) => {children}} 174 | 175 | ) 176 | 177 | const App = withRegistry(registry)(AppPresenter) 178 | 179 | expectText(, 'content') 180 | }) 181 | }) 182 | 183 | describe('overwrite', () => { 184 | test('should overwrite component in registry', () => { 185 | const baseRegistry = new Registry({ id: 'registry' }) 186 | const overwriteRegistry = new Registry({ id: 'registry' }) 187 | const Element = () => content 188 | const ElementOverwritten = () => overwritten 189 | 190 | baseRegistry.fill({ Element }) 191 | overwriteRegistry.fill({ Element: ElementOverwritten }) 192 | 193 | const AppPresenter: React.FC = () => ( 194 | {({ Element }) => } 195 | ) 196 | 197 | const App = withRegistry(overwriteRegistry, baseRegistry)(AppPresenter) 198 | 199 | expectText(, 'overwritten') 200 | }) 201 | 202 | test('should partially overwrite components in registry', () => { 203 | const baseRegistry = new Registry({ id: 'registry' }) 204 | const overwriteRegistry = new Registry({ id: 'registry' }) 205 | const Element = () => content 206 | const ElementOverwritten = () => overwritten 207 | const Extra = () => extra 208 | 209 | baseRegistry.fill({ Element, Extra }) 210 | overwriteRegistry.fill({ Element: ElementOverwritten }) 211 | 212 | const AppPresenter: React.FC = () => ( 213 | {({ Element }) => } 214 | ) 215 | 216 | const App = withRegistry(overwriteRegistry, baseRegistry)(AppPresenter) 217 | 218 | expectText(, 'overwritten') 219 | }) 220 | }) 221 | 222 | describe('extend', () => { 223 | test('should extend component in registry', () => { 224 | const baseRegistry = new Registry({ id: 'registry' }) 225 | const extendedRegistry = new Registry({ id: 'registry' }) 226 | const superExtendedRegistry = new Registry({ id: 'registry' }) 227 | const Element: React.FC = () => content 228 | 229 | baseRegistry.fill({ Element }) 230 | extendedRegistry.extends>('Element', (Base) => () => ( 231 |
232 | extended 233 |
234 | )) 235 | superExtendedRegistry.extends>('Element', (Base) => () => ( 236 |
237 | super 238 |
239 | )) 240 | 241 | const AppPresenter: React.FC = () => ( 242 | {({ Element }) => } 243 | ) 244 | 245 | const App = withRegistry(baseRegistry)(AppPresenter) 246 | const AppExtended = withRegistry(extendedRegistry)(App) 247 | const AppSuperExtended = withRegistry(superExtendedRegistry)(AppExtended) 248 | 249 | expectText(, 'content') 250 | expectText(, 'extended content') 251 | expectText(, 'super extended content') 252 | }) 253 | 254 | test('should partially extend components in registry', () => { 255 | const baseRegistry = new Registry({ id: 'registry' }) 256 | const extendedLeftRegistry = new Registry({ id: 'registry' }) 257 | const extendedRightRegistry = new Registry({ id: 'registry' }) 258 | const Left: React.FC = () => left 259 | const Right: React.FC = () => right 260 | const Extension = (Base: React.FC) => () => 261 | ( 262 |
263 | extended 264 |
265 | ) 266 | 267 | baseRegistry.fill({ Left, Right }) 268 | extendedLeftRegistry.extends>('Left', Extension) 269 | extendedRightRegistry.extends>('Right', Extension) 270 | 271 | const AppPresenter: React.FC = () => ( 272 | 273 | {({ Left, Right }) => ( 274 | <> 275 | 276 | 277 | 278 | )} 279 | 280 | ) 281 | 282 | const App = withRegistry(baseRegistry)(AppPresenter) 283 | const AppExtended = withRegistry(extendedLeftRegistry, extendedRightRegistry)(App) 284 | 285 | expectText(, 'leftright') 286 | expectText(, 'extended leftextended right') 287 | }) 288 | 289 | test('should extend other values in registry', () => { 290 | const baseRegistry = new Registry({ id: 'registry' }).fill({ 291 | prop: 'foo', 292 | functionProp: () => 'bar', 293 | }) 294 | const extendedRegistry = new Registry({ id: 'registry' }) 295 | extendedRegistry.extends('prop', (Base) => `extended ${Base}`) 296 | extendedRegistry.extends<() => String>('functionProp', (Base) => () => `extended ${Base()}`) 297 | 298 | const AppPresenter: React.FC = () => ( 299 | 300 | {({ prop, functionProp }) => ( 301 |
302 | {prop} / {functionProp()} 303 |
304 | )} 305 |
306 | ) 307 | 308 | const App = withRegistry(baseRegistry)(AppPresenter) 309 | const AppExtended = withRegistry(extendedRegistry)(App) 310 | 311 | expectText(, 'foo / bar') 312 | expectText(, 'extended foo / extended bar') 313 | }) 314 | 315 | test('should not influence adjacent context', () => { 316 | const registry = new Registry({ id: 'registry' }) 317 | const otherRegistryExtended = new Registry({ id: 'other-registry' }) 318 | const Element: React.FC = () => content 319 | 320 | registry.fill({ Element }) 321 | otherRegistryExtended.extends>('Element', (Base) => () => ( 322 |
323 | extended 324 |
325 | )) 326 | 327 | const AppPresenter: React.FC = () => ( 328 | {({ Element }) => } 329 | ) 330 | 331 | const App = withRegistry(otherRegistryExtended, registry)(AppPresenter) 332 | 333 | expectText(, 'content') 334 | }) 335 | }) 336 | }) 337 | }) 338 | --------------------------------------------------------------------------------