diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ab84a1a..23afef4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,7 @@ name: Release on: push: - branches: master + branches: [master, next, next-major, beta, alpha] jobs: release: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3cf7d1b..1da5c2d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ on: push: branches: [develop] pull_request: - branches: [master, develop] + branches: [develop, master, next, next-major, beta, alpha] jobs: test: diff --git a/.xo-config b/.xo-config deleted file mode 100644 index 537ca57..0000000 --- a/.xo-config +++ /dev/null @@ -1,11 +0,0 @@ -{ - "rules": { - "comma-dangle": ["error", "always-multiline"] - }, - "overrides": [ - { - "files": "test/*", - "env": "jest" - } - ] -} \ No newline at end of file diff --git a/LICENSE b/LICENSE index 957a161..dff8850 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Hiroki Osame +Copyright (c) 2020 Hiroki Osame Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index b9dddf3..474229c 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,93 @@ -# Pkg Name +# postcss-custom-properties-transformer [![Latest version](https://badgen.net/npm/v/postcss-custom-properties-transformer)](https://npm.im/postcss-custom-properties-transformer) [![Monthly downloads](https://badgen.net/npm/dm/postcss-custom-properties-transformer)](https://npm.im/postcss-custom-properties-transformer) [![Install size](https://packagephobia.now.sh/badge?p=postcss-custom-properties-transformer)](https://packagephobia.now.sh/result?p=postcss-custom-properties-transformer) -Description +PostCSS plugin to transform [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/--*) -## :raising_hand: Why? +## πŸ™‹β€β™‚οΈ Why? +Transform CSS custom properties to... -## :rocket: Install +- **πŸ”₯ Scope to package** Namespace them to your library to prevent collision with other custom properties +- **πŸ‘Œ Scope to file** Scope to a file to prevent collision with other files +- **πŸ₯ Minify** Shorten long custom properties via hashing +- **🀬 Obfuscate** Mangle custom-property names to deter reverse-engineering + +## πŸš€ Install ```sh -npm i pkg-name +npm i -D postcss postcss-custom-properties-transformer +``` + +## 🚦 Quick Setup + +Add `postcss-custom-properties-transformer` to your PostCSS configuration (eg. `postcss.config.js`) and pass in a `transformer` function. + +> Warning: this plugin doesn't validate custom properties. [Make sure to not use invalid characters (eg. period)](https://stackoverflow.com/a/42311038) + +```diff +module.exports = { + plugins: [ + ++ // Insert above plugins that read custom properties ++ require('postcss-custom-properties-transformer')({ ++ transformer({ property }) { ++ // Prefixing all custom properties with 'APP-' ++ return 'APP-' + property; ++ } ++ }), + + require('postcss-preset-env')() + ] +}; +``` + +## πŸ‘¨πŸ»β€πŸ« Examples + +### Namespace with package meta +If you have a CSS library, you can scope your custom properties to every release so that multiple versions of the library used in the same app doesn't yield any collisions. + +```js +const pkg = require('./package.json') +require('postcss-custom-properties-transformer')({ + transformer({ property }) { + return `${pkg.name}-${pkg.version}-${property}`; + } +}) +``` + +### Hash custom properties +If you want to hash your custom properties to shorten/obfuscate them, pass in a hashing algorithm of your choice. + +This demo uses a 6-character truncated MD5 hash; MD5 and the SHA-family has [statistically good uniform distribution](https://stackoverflow.com/questions/8184941/uniform-distribution-of-truncated-md5) and can be truncated. + +However, note that the shorter the hash, the higher the collision rate. There will be a warning if a collision is detected. + +```js +const crypto = require('crypto'); +const md5 = string => crypto.createHash('md5').update(string).digest('hex'); + +require('postcss-custom-properties-transformer')({ + transformer({ property }) { + return md5(property).slice(0, 6); + } +}) ``` +### Advanced transformations +If you want to do something more complexβ€”such as discriminate between global and local custom properties (eg. theme variables). + +```js +require('postcss-custom-properties-transformer')({ + transformer({ property }) { + if (property.startsWith('theme-')) { + return property; + } + return hash(property); + } +}) +``` +## βš™οΈ Options +- `transformer(data)` `` + - `data` `` + - `property` `` - The custom property name that's being transformed. The `--` prefix is omitted + - `filepath` `` - The path to the file where the custom property is being transformed + - `css` `` - The entire CSS code of the file diff --git a/lib/index.js b/lib/index.js index e69de29..4b96f7f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -0,0 +1,65 @@ +const postcss = require('postcss'); +const assert = require('assert'); +const processPropertyValue = require('./process-property-value'); + +const {stringify: string} = JSON; + +// From postcss-custom-properties +// https://github.com/postcss/postcss-custom-properties/blob/1e1ef6b/src/lib/transform-properties.js#L37 +const customPropertyPtrn = /^--[A-z][\w-]*$/; +const varFnPtrn = /(^|[^\w-])var\([\W\w]+\)/; +const name = 'postcss-custom-properties-transformer'; + +function markHashUsed(map, propertyName, originalPropertyName) { + if (!map.has(propertyName)) { + map.set(propertyName, originalPropertyName); + return; + } + + const previousPropName = map.get(propertyName); + if (originalPropertyName !== previousPropName) { + console.warn(`[${name}] Collision: property name ${string(propertyName)} was generated from input ${string(originalPropertyName)} but was already generated from input ${string(previousPropName)}`); + } +} + +const createTransformer = transformer => { + const usedPropertyNames = new Map(); + + return (customProperty, data) => { + data.property = customProperty.slice(2); + + const newPropertyName = '--' + transformer(data); + + markHashUsed(usedPropertyNames, newPropertyName, customProperty); + + return newPropertyName; + }; +}; + +const customPropertiesTransformer = postcss.plugin(name, options => { + assert(options && options.transformer, `[${name}] a transformer must be passed in`); + assert(typeof options.transformer === 'function', `[${name}] transformer must be a function`); + + const transformer = createTransformer(options.transformer); + + return root => { + const data = Object.create({ + filepath: root.source.input.file, + css: root.source.input.css, + }); + + root.walkDecls(decl => { + // Custom property declaration + if (customPropertyPtrn.test(decl.prop)) { + decl.prop = transformer(decl.prop, data); + } + + // Custom property usage + if (varFnPtrn.test(decl.value)) { + decl.value = processPropertyValue(decl.value, customProperty => transformer(customProperty, data)); + } + }); + }; +}); + +module.exports = customPropertiesTransformer; diff --git a/lib/process-property-value.js b/lib/process-property-value.js new file mode 100644 index 0000000..6a122fe --- /dev/null +++ b/lib/process-property-value.js @@ -0,0 +1,29 @@ +const {parse} = require('postcss-values-parser'); + +function transformAstVars(root, transformer) { + if (!root.nodes) { + return; + } + + root.nodes.forEach(node => { + if (node.isVar) { + node.nodes.forEach(node2 => { + if (node2.isVariable) { + node2.value = transformer(node2.value); + } + }); + } else { + transformAstVars(node, transformer); + } + }); +} + +function processPropertyValue(value, cb) { + const parsed = parse(value); + + transformAstVars(parsed, cb); + + return parsed.toString(); +} + +module.exports = processPropertyValue; diff --git a/package-lock.json b/package-lock.json index 298d580..76a2c36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "NODE-PACKAGE-TEMPLATE", + "name": "postcss-custom-properties-transformer", "version": "0.0.0-semantic-release", "lockfileVersion": 1, "requires": true, @@ -1890,8 +1890,7 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "combined-stream": { "version": "1.0.8", @@ -2548,8 +2547,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escodegen": { "version": "1.14.3", @@ -4622,6 +4620,14 @@ "unc-path-regex": "^0.1.2" } }, + "is-url-superb": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-url-superb/-/is-url-superb-3.0.0.tgz", + "integrity": "sha512-3faQP+wHCGDQT1qReM5zCPx2mxoal6DzbzquFlCYJLWyy4WPTved33ea2xFbX37z4NoriEwZGIYhFtx8RUB5wQ==", + "requires": { + "url-regex": "^5.0.0" + } + }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -6530,6 +6536,83 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, + "postcss": { + "version": "7.0.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz", + "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-values-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-3.2.1.tgz", + "integrity": "sha512-SQ7/88VE9LhJh9gc27/hqnSU/aZaREVJcRVccXBmajgP2RkjdJzNyH/a9GCVMI5nsRhT0jC5HpUMwfkz81DVVg==", + "requires": { + "color-name": "^1.1.4", + "is-url-superb": "^3.0.0", + "postcss": "^7.0.5", + "url-regex": "^5.0.0" + } + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -7028,6 +7111,15 @@ "inherits": "^2.0.1" } }, + "rollup": { + "version": "2.26.8", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.26.8.tgz", + "integrity": "sha512-li9WaJYc5z9WzV1jhZbPQCrsOpGNsI+Li1qyrn5n745ZNSnlkRlBtj1Hs+Z0Dc2N1+P7HT34UKAEASqN9Th8cg==", + "dev": true, + "requires": { + "fsevents": "~2.1.2" + } + }, "rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", @@ -7476,8 +7568,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "source-map-resolve": { "version": "0.5.3", @@ -7934,6 +8025,11 @@ "setimmediate": "^1.0.4" } }, + "tlds": { + "version": "1.209.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.209.0.tgz", + "integrity": "sha512-KVsZ1NSpBodpo42/JIwTyau7SqUxV/qQMp2epSDPa99885LpHWLaVCCt8CWzGe4X5YIVNr+b6bUys9e9eEb5OA==" + }, "tmpl": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", @@ -8284,6 +8380,22 @@ "prepend-http": "^2.0.0" } }, + "url-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/url-regex/-/url-regex-5.0.0.tgz", + "integrity": "sha512-O08GjTiAFNsSlrUWfqF1jH0H1W3m35ZyadHrGv5krdnmPPoxP27oDTqux/579PtaroiSGm5yma6KT1mHFH6Y/g==", + "requires": { + "ip-regex": "^4.1.0", + "tlds": "^1.203.0" + }, + "dependencies": { + "ip-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.1.0.tgz", + "integrity": "sha512-pKnZpbgCTfH/1NLIlOduP/V+WRXzC2MOz3Qo8xmxk8C5GudJLgK5QyLVXOSWy3ParAH7Eemurl3xjv/WXYFvMA==" + } + } + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", diff --git a/package.json b/package.json index 4a48fbc..a88a2a9 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,20 @@ { - "name": "NODE-PACKAGE-TEMPLATE", + "name": "postcss-custom-properties-transformer", "version": "0.0.0-semantic-release", - "description": "Template for Node package repos", - "keywords": [], + "description": "PostCSS plugin to transform custom properties", + "keywords": [ + "postcss", + "plugin", + "custom properties", + "transformer" + ], "license": "MIT", - "repository": "privatenumber/NODE-PACKAGE-TEMPLATE", + "repository": "privatenumber/postcss-custom-properties-transformer", "author": "Hiroki Osame ", "files": [ - "index.js" + "lib" ], - "main": "index.js", + "main": "lib/index.js", "scripts": { "test": "jest", "lint": "xo" @@ -20,12 +25,19 @@ } }, "lint-staged": { - "*.js": "xo" + "*.js": [ + "xo", + "jest --bail --findRelatedTests" + ] }, "devDependencies": { "husky": "^4.2.5", "jest": "^26.4.2", "lint-staged": "^10.2.13", "xo": "^0.33.0" + }, + "dependencies": { + "postcss": "^7.0.32", + "postcss-values-parser": "^3.2.1" } } diff --git a/test/__snapshots__/index.spec.js.snap b/test/__snapshots__/index.spec.js.snap new file mode 100644 index 0000000..f918968 --- /dev/null +++ b/test/__snapshots__/index.spec.js.snap @@ -0,0 +1,169 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`transformer function custom hash - scoped to file 1`] = ` +Array [ + " + .button { + --ae7621: red; + --b682a4: 24px; + background: var(--ae7621); + border-color: var(--ae7621); + font-size: var(--b682a4); + } + + .input { + --ae7621: red; + --b682a4: 24px; + background: var(--ae7621); + border-color: var(--ae7621); + font-size: var(--b682a4); + border: 1px solid var(--ae7621); + } + ", + " + :root { + --199650: red; + --29d9c5: 24px; + --d85b32: calc(var(--29d9c5) * 2); + } + .section { + background: var(--199650); + border-color: var(--199650); + font-size: var(--29d9c5); + } + + .heading { + background: var(--199650); + border-color: var(--199650); + font-size: var(--29d9c5); + border: 1px solid var(--199650); + } + ", +] +`; + +exports[`transformer function custom hash 1`] = ` +Array [ + " + .button { + --70dda5: red; + --9b1d0a: 24px; + background: var(--70dda5); + border-color: var(--70dda5); + font-size: var(--9b1d0a); + } + + .input { + --70dda5: red; + --9b1d0a: 24px; + background: var(--70dda5); + border-color: var(--70dda5); + font-size: var(--9b1d0a); + border: 1px solid var(--70dda5); + } + ", + " + :root { + --70dda5: red; + --9b1d0a: 24px; + --defe4c: calc(var(--9b1d0a) * 2); + } + .section { + background: var(--70dda5); + border-color: var(--70dda5); + font-size: var(--9b1d0a); + } + + .heading { + background: var(--70dda5); + border-color: var(--70dda5); + font-size: var(--9b1d0a); + border: 1px solid var(--70dda5); + } + ", +] +`; + +exports[`transformer function emoji namespace 1`] = ` +Array [ + " + .button { + --πŸ”₯-color: red; + --πŸ”₯-font-size: 24px; + background: var(--πŸ”₯-color); + border-color: var(--πŸ”₯-color); + font-size: var(--πŸ”₯-font-size); + } + + .input { + --πŸ”₯-color: red; + --πŸ”₯-font-size: 24px; + background: var(--πŸ”₯-color); + border-color: var(--πŸ”₯-color); + font-size: var(--πŸ”₯-font-size); + border: 1px solid var(--πŸ”₯-color); + } + ", + " + :root { + --πŸ”₯-color: red; + --πŸ”₯-font-size: 24px; + --πŸ”₯-large-font-size: calc(var(--πŸ”₯-font-size) * 2); + } + .section { + background: var(--πŸ”₯-color); + border-color: var(--πŸ”₯-color); + font-size: var(--πŸ”₯-font-size); + } + + .heading { + background: var(--πŸ”₯-color); + border-color: var(--πŸ”₯-color); + font-size: var(--πŸ”₯-font-size); + border: 1px solid var(--πŸ”₯-color); + } + ", +] +`; + +exports[`transformer function namespace 1`] = ` +Array [ + " + .button { + --app-color: red; + --app-font-size: 24px; + background: var(--app-color); + border-color: var(--app-color); + font-size: var(--app-font-size); + } + + .input { + --app-color: red; + --app-font-size: 24px; + background: var(--app-color); + border-color: var(--app-color); + font-size: var(--app-font-size); + border: 1px solid var(--app-color); + } + ", + " + :root { + --app-color: red; + --app-font-size: 24px; + --app-large-font-size: calc(var(--app-font-size) * 2); + } + .section { + background: var(--app-color); + border-color: var(--app-color); + font-size: var(--app-font-size); + } + + .heading { + background: var(--app-color); + border-color: var(--app-color); + font-size: var(--app-font-size); + border: 1px solid var(--app-color); + } + ", +] +`; diff --git a/test/fixtures.js b/test/fixtures.js new file mode 100644 index 0000000..d9278aa --- /dev/null +++ b/test/fixtures.js @@ -0,0 +1,41 @@ +const fixtures = { + 'file-a.css': ` + .button { + --color: red; + --font-size: 24px; + background: var(--color); + border-color: var(--color); + font-size: var(--font-size); + } + + .input { + --color: red; + --font-size: 24px; + background: var(--color); + border-color: var(--color); + font-size: var(--font-size); + border: 1px solid var(--color); + } + `, + 'file-b.css': ` + :root { + --color: red; + --font-size: 24px; + --large-font-size: calc(var(--font-size) * 2); + } + .section { + background: var(--color); + border-color: var(--color); + font-size: var(--font-size); + } + + .heading { + background: var(--color); + border-color: var(--color); + font-size: var(--font-size); + border: 1px solid var(--color); + } + `, +}; + +module.exports = fixtures; diff --git a/test/index.spec.js b/test/index.spec.js index e69de29..3f2d791 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -0,0 +1,71 @@ +const {transform, md5} = require('./utils'); +const fixtures = require('./fixtures'); + +describe('error handling', () => { + test('no options', () => { + expect(() => transform(fixtures)).toThrowError('[postcss-custom-properties-transformer] a transformer must be passed in'); + }); + + test('no transformer', () => { + expect(() => transform(fixtures, {})).toThrowError('[postcss-custom-properties-transformer] a transformer must be passed in'); + }); + + test('invalid transformer', () => { + expect(() => transform(fixtures, { + transformer: 123, + })).toThrowError('[postcss-custom-properties-transformer] transformer must be a function'); + }); + + test('transformer collision warning', async () => { + const spy = jest.spyOn(global.console, 'warn').mockImplementation(); + await transform(fixtures, { + transformer() { + return 'constant-value'; + }, + }); + expect(spy).toHaveBeenCalledWith('[postcss-custom-properties-transformer] Collision: property name "--constant-value" was generated from input "--font-size" but was already generated from input "--color"'); + spy.mockRestore(); + }); +}); + +describe('transformer function', () => { + test('namespace', async () => { + const output = await transform(fixtures, { + transformer({property}) { + return `app-${property}`; + }, + }); + + expect(output).toMatchSnapshot(); + }); + + test('emoji namespace', async () => { + const output = await transform(fixtures, { + transformer({property}) { + return `πŸ”₯-${property}`; + }, + }); + + expect(output).toMatchSnapshot(); + }); + + test('custom hash', async () => { + const output = await transform(fixtures, { + transformer({property}) { + return md5(property).slice(0, 6); + }, + }); + + expect(output).toMatchSnapshot(); + }); + + test('custom hash - scoped to file', async () => { + const output = await transform(fixtures, { + transformer({property, filepath}) { + return md5(filepath + property).slice(0, 6); + }, + }); + + expect(output).toMatchSnapshot(); + }); +}); diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 0000000..a909fca --- /dev/null +++ b/test/utils.js @@ -0,0 +1,23 @@ +const crypto = require('crypto'); +const postcss = require('postcss'); +const postcssCustomPropertiesTransformer = require('..'); + +const md5 = string => crypto.createHash('md5').update(string).digest('hex'); + +const transform = (cssFiles, options) => { + const processor = postcss([ + postcssCustomPropertiesTransformer(options), + ]); + + return Promise.all(Object.entries(cssFiles).map(async ([name, cssFile]) => { + const result = await processor.process(cssFile, { + from: '/' + name, + }); + return result.css; + })); +}; + +module.exports = { + transform, + md5, +}; diff --git a/xo.config.js b/xo.config.js new file mode 100644 index 0000000..041cdf5 --- /dev/null +++ b/xo.config.js @@ -0,0 +1,11 @@ +module.exports = { + rules: { + 'comma-dangle': ['error', 'always-multiline'], + }, + overrides: [ + { + files: 'test/*', + env: 'jest', + }, + ], +};