diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a27b2a6..a3292669 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -120,20 +120,19 @@ jobs: env: MUI_VERSION: ${{ matrix.mui-version }} run: | - bun link - cat >integration-test/package.json <packages/integration-test/package.json <MUI 5 has introduced a new way of styling React components using a Theme-aware -`sx` property. It can be necessary to create your own styled components while still allowing for additional styling -by the consumer. In this case your component will have its own `sx` property, most likely optional. This makes it -necessary somehow to combine your own styles with the styles coming from the consumer of your component. One approach -might be using a styling wrapper, an alternative way to style your component with the -[`styled()` utility](https://mui.com/system/styled/). Thus, you could apply the consumer's `sx` to the pre-styled -component. However, this approach can be inconvenient for several reasons. - -I came to conclusion that merging several `sx` properties is better. However, the `SxProps` has rather complex data -type. It can be an object with styling properties, can be function, can be `null`. It can be a challenge to perform -a merge under strict typing of Typescript. - -## How it works - -Luckily, starting version [5.1.0](https://github.com/mui/material-ui/releases/tag/v5.1.0) of MUI `SxProps` -[can](https://github.com/mui/material-ui/blob/v5.1.0/packages/mui-system/src/styleFunctionSx/styleFunctionSx.d.ts#L60) -also be `array`. However, nested arrays are not allowed, so this utility does exactly the flat merge, also bringing -support for conditional and optional inclusions, providing a very simple and semantically clean interface. - -## Performance - -The utility has been tested to support up to 65535 arguments. - -![Performance chart](https://raw.githubusercontent.com/RobinTail/merge-sx/refs/heads/master/performance.svg) - -## Examples - -### Conditional merging - -The utility supports `false`: - -```tsx - -``` - -### Optional merging - -The utility supports `undefined`: - -```tsx -interface MyButtonProps { - sx?: SxProps; // optional prop for consumer -} - -const MyButton = ({ sx: consumerSx }: MyButtonProps) => ( - -); -``` - -### Inline Theme supplying - -The utility is generic and accepts the type argument. - -```ts -// theme is Theme -mergeSx(sx1, (theme) => ({ mb: theme.spacing(1) })); -``` diff --git a/README.md b/README.md new file mode 120000 index 00000000..8b0ec445 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +packages/merge-sx/README.md \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 908e80c9..6502b45f 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/bunfig.toml b/bunfig.toml index a16a2973..fe89be02 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -1,5 +1,6 @@ [install] peer = false +registry = { url = "https://registry.npmjs.org", token = "$NPM_KEY" } [test] coverage = true # always enable coverage coverageReporter = ["text", "lcov"] diff --git a/package.json b/package.json index 1ec9cfed..9a77cb90 100644 --- a/package.json +++ b/package.json @@ -1,77 +1,27 @@ { - "name": "merge-sx", - "version": "3.2.4", - "description": "Combines multiple SxProps for Material UI components.", - "main": "dist/index.cjs", - "module": "dist/index.js", - "types": "dist/index.d.ts", - "type": "module", - "exports": { - ".": { - "import": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - }, - "require": { - "types": "./dist/index.d.cts", - "default": "./dist/index.cjs" - } - } - }, - "files": [ - "dist", - "*.md" - ], - "sideEffects": false, - "repository": { - "type": "git", - "url": "git+https://github.com/RobinTail/merge-sx.git" - }, - "homepage": "https://github.com/RobinTail/merge-sx", + "private": true, + "workspaces": ["packages/*"], "author": { "name": "Anna Bocharova", "url": "https://robintail.cz" }, - "bugs": "https://github.com/RobinTail/merge-sx/issues", - "funding": "https://github.com/sponsors/RobinTail", "license": "MIT", "scripts": { - "build": "tsdown && attw --pack", + "build": "bun run --cwd packages/merge-sx build", "lint": "bun run biome check", - "test": "tsc --noEmit && bun test src", - "intTest": "tsc -p integration-test/tsconfig.json && bun test integration-test", - "mdfix": "bunx --bun prettier *.md --write", - "postversion": "git push && git push --tags", - "prepublishOnly": "bun run lint && bun run test && bun run build" - }, - "peerDependencies": { - "@mui/material": "^5.1.0 || ^6.0.0" + "test": "bun test --filter merge-sx", + "intTest": "tsc -p packages/integration-test && bun test --filter integration-test", + "mdfix": "bunx --bun prettier packages/merge-sx/*.md --write", + "version": "bun run tools/version.ts" }, "devDependencies": { "@arethetypeswrong/cli": "^0.17.0", "@biomejs/biome": "1.9.4", - "@emotion/styled": "^11.13.0", - "@mui/material": "^6.1.2", "@tsconfig/bun": "^1.0.7", "@types/bun": "latest", - "react": "^18.3.1", - "tsdown": "^0.3.0", - "typescript": "^5.6.2" - }, - "keywords": [ - "react", - "typescript", - "styled-components", - "reactjs", - "material-ui", - "styling", - "css-in-js", - "jss", - "strict", - "mui", - "merge", - "combine", - "merging", - "muiv5" - ] + "@types/semver": "^7.5.8", + "semver": "^7.6.3", + "tsdown": "^0.3.1", + "typescript": "^5.7.2" + } } diff --git a/integration-test/.gitignore b/packages/integration-test/.gitignore similarity index 100% rename from integration-test/.gitignore rename to packages/integration-test/.gitignore diff --git a/integration-test/integration.spec.ts b/packages/integration-test/integration.spec.ts similarity index 100% rename from integration-test/integration.spec.ts rename to packages/integration-test/integration.spec.ts diff --git a/integration-test/tsconfig.json b/packages/integration-test/tsconfig.json similarity index 66% rename from integration-test/tsconfig.json rename to packages/integration-test/tsconfig.json index a71ff2be..cbb904ee 100644 --- a/integration-test/tsconfig.json +++ b/packages/integration-test/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../tsconfig.json", + "extends": "../../tsconfig.json", "include": ["*.ts"], "compilerOptions": { "noEmit": true diff --git a/CHANGELOG.md b/packages/merge-sx/CHANGELOG.md similarity index 95% rename from CHANGELOG.md rename to packages/merge-sx/CHANGELOG.md index 307b823d..900f4e2a 100644 --- a/CHANGELOG.md +++ b/packages/merge-sx/CHANGELOG.md @@ -2,6 +2,13 @@ ## Version 3 +### v3.3.0 + +- Technical update: + - Upgraded all dependencies and tested on latest MUI 6; + - Implemented workspaces to avoid duplicated dependencies for the integration test; + - Releasing using `bun publish`. + ### v3.2.3 - Dependencies update: diff --git a/packages/merge-sx/LICENSE b/packages/merge-sx/LICENSE new file mode 100644 index 00000000..c9112185 --- /dev/null +++ b/packages/merge-sx/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Anna Bocharova + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/merge-sx/README.md b/packages/merge-sx/README.md new file mode 100644 index 00000000..357b33db --- /dev/null +++ b/packages/merge-sx/README.md @@ -0,0 +1,91 @@ +# merge-sx + +[![Coverage Status](https://coveralls.io/repos/github/RobinTail/merge-sx/badge.svg?branch=master)](https://coveralls.io/github/RobinTail/merge-sx?branch=master) +![NPM Downloads](https://img.shields.io/npm/dw/merge-sx) +![NPM License](https://img.shields.io/npm/l/merge-sx) + +Combines multiple [`SxProps`](https://mui.com/system/getting-started/the-sx-prop) +for [Material UI](https://mui.com/) components. + +## Installation + +```shell +npm install merge-sx +# or +yarn add merge-sx +``` + +## Usage + +The utility provides a very simple and semantically clean interface, that supports conditional and optional inclusions. + +```ts +import { mergeSx } from "merge-sx"; + +// Merge your SxProps +mergeSx(sx1, sx2 /*, ... */); +// Merge optionally +mergeSx(internalSx, props?.sx); // supports undefined +// Merge conditionally +mergeSx(commonSx, isMobile && mobileSx); // supports false +``` + +## Why might you need it + +MUI 5 has introduced a new way of styling React components using a Theme-aware +`sx` property. It can be necessary to create your own styled components while still allowing for additional styling +by the consumer. In this case your component will have its own `sx` property, most likely optional. This makes it +necessary somehow to combine your own styles with the styles coming from the consumer of your component. One approach +might be using a styling wrapper, an alternative way to style your component with the +[`styled()` utility](https://mui.com/system/styled/). Thus, you could apply the consumer's `sx` to the pre-styled +component. However, this approach can be inconvenient for several reasons. + +I came to conclusion that merging several `sx` properties is better. However, the `SxProps` has rather complex data +type. It can be an object with styling properties, can be function, can be `null`. It can be a challenge to perform +a merge under strict typing of Typescript. + +## How it works + +Luckily, starting version [5.1.0](https://github.com/mui/material-ui/releases/tag/v5.1.0) of MUI `SxProps` +[can](https://github.com/mui/material-ui/blob/v5.1.0/packages/mui-system/src/styleFunctionSx/styleFunctionSx.d.ts#L60) +also be `array`. However, nested arrays are not allowed, so this utility does exactly the flat merge, also bringing +support for conditional and optional inclusions, providing a very simple and semantically clean interface. + +## Performance + +The utility has been tested to support up to 65535 arguments. + +![Performance chart](https://raw.githubusercontent.com/RobinTail/merge-sx/refs/heads/master/performance.svg) + +## Examples + +### Conditional merging + +The utility supports `false`: + +```tsx + +``` + +### Optional merging + +The utility supports `undefined`: + +```tsx +interface MyButtonProps { + sx?: SxProps; // optional prop for consumer +} + +const MyButton = ({ sx: consumerSx }: MyButtonProps) => ( + +); +``` + +### Inline Theme supplying + +The utility is generic and accepts the type argument. + +```ts +// theme is Theme +mergeSx(sx1, (theme) => ({ mb: theme.spacing(1) })); +``` diff --git a/packages/merge-sx/package.json b/packages/merge-sx/package.json new file mode 100644 index 00000000..b05e13c2 --- /dev/null +++ b/packages/merge-sx/package.json @@ -0,0 +1,64 @@ +{ + "name": "merge-sx", + "version": "3.3.0-beta.8", + "description": "Combines multiple SxProps for Material UI components.", + "main": "dist/index.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + } + }, + "files": [ + "dist", + "*.md" + ], + "sideEffects": false, + "repository": { + "type": "git", + "url": "git+https://github.com/RobinTail/merge-sx.git" + }, + "homepage": "https://github.com/RobinTail/merge-sx", + "author": { + "name": "Anna Bocharova", + "url": "https://robintail.cz" + }, + "scripts": { + "build": "tsdown && attw --pack", + "test": "tsc --noEmit && bun test", + "prepublishOnly": "bun biome check && bun run test && bun run build" + }, + "peerDependencies": { + "@mui/material": "^5.1.0 || ^6.0.0" + }, + "devDependencies": { + "@emotion/styled": "^11.13.5", + "@mui/material": "^6.1.9", + "react": "^18.3.1" + }, + "keywords": [ + "react", + "typescript", + "styled-components", + "reactjs", + "material-ui", + "styling", + "css-in-js", + "jss", + "strict", + "mui", + "merge", + "combine", + "merging", + "muiv5" + ] +} diff --git a/src/index.spec.ts b/packages/merge-sx/src/index.spec.ts similarity index 100% rename from src/index.spec.ts rename to packages/merge-sx/src/index.spec.ts diff --git a/src/index.ts b/packages/merge-sx/src/index.ts similarity index 100% rename from src/index.ts rename to packages/merge-sx/src/index.ts diff --git a/packages/merge-sx/tsconfig.json b/packages/merge-sx/tsconfig.json new file mode 100644 index 00000000..596e2cf7 --- /dev/null +++ b/packages/merge-sx/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"] +} diff --git a/tsdown.config.ts b/packages/merge-sx/tsdown.config.ts similarity index 100% rename from tsdown.config.ts rename to packages/merge-sx/tsdown.config.ts diff --git a/tools/benchmark.bench.ts b/tools/benchmark.bench.ts index ed45f263..befd5c56 100644 --- a/tools/benchmark.bench.ts +++ b/tools/benchmark.bench.ts @@ -1,6 +1,6 @@ import type { SxProps, Theme } from "@mui/system"; import { bench, describe } from "vitest"; -import { mergeSx } from "../src"; +import { mergeSx } from "../packages/merge-sx/src"; describe("Performance", () => { for (const count of [10, 100, 1000, 10000]) { diff --git a/tools/version.ts b/tools/version.ts new file mode 100644 index 00000000..3a0f6476 --- /dev/null +++ b/tools/version.ts @@ -0,0 +1,53 @@ +import { fail } from "node:assert"; +import { $ } from "bun"; +import { type ReleaseType, inc, valid as isValidVersion } from "semver"; + +const path = "./packages/merge-sx/package.json"; +const variants: ReleaseType[] = [ + "major", + "premajor", + "minor", + "preminor", + "patch", + "prepatch", + "prerelease", +]; + +const isValidTarget = (subject: string): subject is ReleaseType => + (variants as string[]).includes(subject); + +const isDirty = async () => (await $`git status --porcelain`.quiet()).text(); + +const target = Bun.argv.pop(); +const json = await Bun.file(path).json(); +const { version: current } = json; + +if (!isValidVersion(current)) + throw new Error(`Invalid current version ${current}`); + +if (await isDirty()) + throw new Error( + "There are uncommitted changes. Commit them before releasing.", + ); + +const desired = isValidVersion(target) + ? target + : target && isValidTarget(target) + ? inc(current, target, "beta", "1") + : fail("invalid target version"); + +if (!desired) throw new Error("Failed to bump"); +console.debug(current, "—>", desired); + +const output = JSON.stringify( + Object.assign(json, { version: desired }), + null, + 2, +); +await Bun.write(path, `${output}\n`); + +await $`git add ${path}`; +await $`git commit -m v${desired}`; +await $`git tag v${desired}`; +await $`git push`; +await $`git push --tags`; diff --git a/tsconfig.json b/tsconfig.json index 1910f7a6..eba46e70 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,4 @@ { "extends": "@tsconfig/bun/tsconfig.json", - "compilerOptions": {}, - "include": ["src"] + "compilerOptions": {} }