From 617daa6f7995ee3de30e1c147bbacbf95cafb321 Mon Sep 17 00:00:00 2001 From: Lucas Arcoverde Date: Wed, 22 Nov 2023 14:36:27 -0300 Subject: [PATCH] feat(stylelint): add @vtex/shoreline-stylelint package --- packages/stylelint/CHANGELOG.md | 4 ++ packages/stylelint/package.json | 48 +++++++++++++ packages/stylelint/src/index.js | 29 ++++++++ packages/stylelint/src/index.types.d.ts | 2 + .../src/plugins/no-space-px-values/index.js | 67 +++++++++++++++++++ .../plugins/no-space-px-values/index.test.js | 43 ++++++++++++ .../src/plugins/no-text-property/index.js | 62 +++++++++++++++++ .../plugins/no-text-property/index.test.js | 39 +++++++++++ .../src/utils/replace-declaration.mjs | 13 ++++ packages/stylelint/tsconfig.json | 17 +++++ packages/stylelint/tsup.config.ts | 11 +++ 11 files changed, 335 insertions(+) create mode 100644 packages/stylelint/CHANGELOG.md create mode 100644 packages/stylelint/package.json create mode 100644 packages/stylelint/src/index.js create mode 100644 packages/stylelint/src/index.types.d.ts create mode 100644 packages/stylelint/src/plugins/no-space-px-values/index.js create mode 100644 packages/stylelint/src/plugins/no-space-px-values/index.test.js create mode 100644 packages/stylelint/src/plugins/no-text-property/index.js create mode 100644 packages/stylelint/src/plugins/no-text-property/index.test.js create mode 100644 packages/stylelint/src/utils/replace-declaration.mjs create mode 100644 packages/stylelint/tsconfig.json create mode 100644 packages/stylelint/tsup.config.ts diff --git a/packages/stylelint/CHANGELOG.md b/packages/stylelint/CHANGELOG.md new file mode 100644 index 0000000000..e4d87c4d45 --- /dev/null +++ b/packages/stylelint/CHANGELOG.md @@ -0,0 +1,4 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. diff --git a/packages/stylelint/package.json b/packages/stylelint/package.json new file mode 100644 index 0000000000..e2b783ecee --- /dev/null +++ b/packages/stylelint/package.json @@ -0,0 +1,48 @@ +{ + "name": "@vtex/shoreline-stylelint", + "version": "0.0.0", + "main": "./dist/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/index.d.ts", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "files": [ + "dist" + ], + "exports": { + ".": { + "require": "./dist/index.js", + "import": "./dist/esm/index.js", + "types": "./dist/index.d.ts" + } + }, + "engines": { + "node": ">=16" + }, + "scripts": { + "prebuild": "rm -rf dist", + "dev": "tsup --watch", + "build": "npm run prebuild && tsup", + "test": "jest" + }, + "repository": { + "directory": "packages/stylelint", + "type": "git", + "url": "git+https://github.com/vtex/shoreline.git" + }, + "bugs": { + "url": "https://github.com/vtex/shoreline/issues" + }, + "peerDependencies": { + "stylelint": "^14.15.0 || ^15.0.0" + }, + "devDependencies": { + "tsup": "7.2.0" + }, + "dependencies": {}, + "jest": { + "preset": "jest-preset-stylelint" + } +} diff --git a/packages/stylelint/src/index.js b/packages/stylelint/src/index.js new file mode 100644 index 0000000000..b2855d14c8 --- /dev/null +++ b/packages/stylelint/src/index.js @@ -0,0 +1,29 @@ +'use strict' + +const textPlugin = require('./plugins/no-text-property') +const spacePlugin = require('./plugins/no-space-px-values') + +const colorRules = { + 'color-no-invalid-hex': [ + true, + { + message: 'Please use a Shoreline color token instead of %s', + }, + ], +} + +const typographyRules = { + 'shoreline/no-text-property': true, +} + +const spaceRules = { + 'shoreline/no-space-px-values': true, +} + +module.exports = { + plugins: [textPlugin, spacePlugin], + reportDescriptionlessDisables: true, + reportNeedlessDisables: true, + reportInvalidScopeDisables: true, + rules: { ...colorRules, ...typographyRules, ...spaceRules }, +} diff --git a/packages/stylelint/src/index.types.d.ts b/packages/stylelint/src/index.types.d.ts new file mode 100644 index 0000000000..de5aae157d --- /dev/null +++ b/packages/stylelint/src/index.types.d.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line @typescript-eslint/consistent-type-imports +declare let testRule: import('jest-preset-stylelint').TestRule diff --git a/packages/stylelint/src/plugins/no-space-px-values/index.js b/packages/stylelint/src/plugins/no-space-px-values/index.js new file mode 100644 index 0000000000..ce51089497 --- /dev/null +++ b/packages/stylelint/src/plugins/no-space-px-values/index.js @@ -0,0 +1,67 @@ +const stylelint = require('stylelint') +const { replaceDeclaration } = require('../../utils/replace-declaration.mjs') + +const { ruleMessages, validateOptions, report } = stylelint.utils + +const ruleName = 'shoreline/no-space-px-values' +const messages = ruleMessages(ruleName, { + expected: (prop, value, expectedValue) => + `Expected "${prop}: ${value}" to be "${prop}: ${expectedValue}".`, +}) + +const spaceProps = [ + 'margin', + 'margin-left', + 'margin-right', + 'margin-top', + 'margin-bottom', + 'padding', + 'padding-left', + 'padding-right', + 'padding-top', + 'padding-bottom', +] + +module.exports = stylelint.createPlugin( + ruleName, + function ruleFunction(primaryOption, secondaryOptionObject, context) { + return function lint(postcssRoot, postcssResult) { + const validOptions = validateOptions(postcssResult, ruleName, { + // No options for now... + }) + + if (!validOptions) return + + const isAutoFixing = Boolean(context.fix) + + postcssRoot.walkDecls((decl) => { + const isSpaceProp = spaceProps.includes(decl.prop) + + const isInvalid = isSpaceProp && decl.value.includes('px') + + if (!isInvalid) return + + const pxUnits = decl.value.split('px').filter((unit) => !!unit) + + const remUnits = pxUnits + .map((unit) => `${Number(unit.trim()) / 16}rem`) + .join(' ') + + if (isAutoFixing) { + replaceDeclaration(decl, remUnits) + } else { + report({ + ruleName, + result: postcssResult, + message: messages.expected(decl.prop, decl.value, remUnits), + node: decl, + word: 'text:', + }) + } + }) + } + } +) + +module.exports.ruleName = ruleName +module.exports.messages = messages diff --git a/packages/stylelint/src/plugins/no-space-px-values/index.test.js b/packages/stylelint/src/plugins/no-space-px-values/index.test.js new file mode 100644 index 0000000000..b3d3bfce9b --- /dev/null +++ b/packages/stylelint/src/plugins/no-space-px-values/index.test.js @@ -0,0 +1,43 @@ +const { ruleName, messages } = require('.') + +// eslint-disable-next-line no-undef +testRule({ + fix: true, + ruleName, + plugins: [__dirname], + config: {}, + accept: [ + { + code: 'margin: 1rem', + description: 'Defining a margin', + }, + { + code: 'padding: 1rem 0.5rem', + description: 'Defining a padding', + }, + { + code: 'padding-left: 1rem', + description: 'Defining a padding-left', + }, + ], + reject: [ + { + code: 'margin: 16px', + fixed: 'margin: 1rem', + description: 'Defining a margin', + message: messages.expected('margin', '16px', '1rem'), + }, + { + code: 'padding: 16px 8px', + description: 'Defining a padding', + fixed: 'padding: 1rem 0.5rem', + message: messages.expected('padding', '16px 8px', '1rem 0.5rem'), + }, + { + code: 'padding-left: 22px', + description: 'Defining a padding-left', + fixed: 'padding-left: 1.375rem', + message: messages.expected('padding-left', '22px', '1.375rem'), + }, + ], +}) diff --git a/packages/stylelint/src/plugins/no-text-property/index.js b/packages/stylelint/src/plugins/no-text-property/index.js new file mode 100644 index 0000000000..4d91890444 --- /dev/null +++ b/packages/stylelint/src/plugins/no-text-property/index.js @@ -0,0 +1,62 @@ +const stylelint = require('stylelint') +const { replaceDeclaration } = require('../../utils/replace-declaration.mjs') + +const { ruleMessages, validateOptions, report } = stylelint.utils + +const ruleName = 'shoreline/no-text-property' +const messages = ruleMessages(ruleName, { + expected: `Expected "text" property to be splited in "font" and "letter-spacing"`, +}) + +const textTokenPrefix = '--sl-text' + +module.exports = stylelint.createPlugin( + ruleName, + function ruleFunction(primaryOption, secondaryOptionObject, context) { + return function lint(postcssRoot, postcssResult) { + const validOptions = validateOptions(postcssResult, ruleName, { + // No options for now... + }) + + if (!validOptions) return + + const isAutoFixing = Boolean(context.fix) + + postcssRoot.walkDecls((decl) => { + const isTextProp = decl.prop === 'text' + + if (!isTextProp) return + + if (isAutoFixing) { + const hasShorelinePrefix = decl.value.includes(textTokenPrefix) + + const newFontValue = hasShorelinePrefix + ? decl.value.replace(')', '-font)') + : decl.value + + const newLetterSpacingValue = hasShorelinePrefix + ? newFontValue.replace('font', 'letter-spacing') + : decl.value + + replaceDeclaration(decl, newFontValue, 'font') + + decl.cloneAfter({ + prop: 'letter-spacing', + value: newLetterSpacingValue, + }) + } else { + report({ + ruleName, + result: postcssResult, + message: messages.expected, + node: decl, + word: 'text:', + }) + } + }) + } + } +) + +module.exports.ruleName = ruleName +module.exports.messages = messages diff --git a/packages/stylelint/src/plugins/no-text-property/index.test.js b/packages/stylelint/src/plugins/no-text-property/index.test.js new file mode 100644 index 0000000000..73db2cfcc0 --- /dev/null +++ b/packages/stylelint/src/plugins/no-text-property/index.test.js @@ -0,0 +1,39 @@ +const { ruleName, messages } = require('.') + +// eslint-disable-next-line no-undef +testRule({ + fix: true, + ruleName, + plugins: [__dirname], + config: {}, + accept: [ + { + code: 'font: var(--sl-text-body-font)', + description: 'Defining a font', + }, + { + code: 'letter-spacing: var(--sl-text-body-letter-spacing)', + description: 'Defining a letter-spacing', + }, + ], + reject: [ + { + code: `text: var(--sl-text-body)`, + fixed: `font: var(--sl-text-body-font);letter-spacing: var(--sl-text-body-letter-spacing)`, + description: 'Defining a text rule', + message: messages.expected, + }, + { + code: `text: test`, + fixed: `font: test;letter-spacing: test`, + description: 'Defining a disallowed include name with a namespace', + message: messages.expected, + }, + { + code: `text: var(--sl-my-custom-text-token)`, + fixed: `font: var(--sl-my-custom-text-token);letter-spacing: var(--sl-my-custom-text-token)`, + description: 'Defining a disallowed include name with a namespace', + message: messages.expected, + }, + ], +}) diff --git a/packages/stylelint/src/utils/replace-declaration.mjs b/packages/stylelint/src/utils/replace-declaration.mjs new file mode 100644 index 0000000000..a8082057c5 --- /dev/null +++ b/packages/stylelint/src/utils/replace-declaration.mjs @@ -0,0 +1,13 @@ +function replaceDeclaration(declaration, newValue, newProp) { + if (declaration.raws.value) { + declaration.raws.prop.raw = newProp || declaration.prop + declaration.raws.value.raw = newValue + } else { + declaration.prop = newProp || declaration.prop + declaration.value = newValue + } +} + +module.exports = { + replaceDeclaration, +} diff --git a/packages/stylelint/tsconfig.json b/packages/stylelint/tsconfig.json new file mode 100644 index 0000000000..e89d51e831 --- /dev/null +++ b/packages/stylelint/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "allowJs": true, + "rootDir": "./src", + "module": "commonjs", + "moduleResolution": "node" + }, + "include": ["./src"], + "exclude": [ + "node_modules", + "dist", + "**/*.test.*", + "**/*.stories.*", + "**/*test-utils*" + ] +} diff --git a/packages/stylelint/tsup.config.ts b/packages/stylelint/tsup.config.ts new file mode 100644 index 0000000000..98d57d9974 --- /dev/null +++ b/packages/stylelint/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/index.js'], + format: ['cjs', 'esm'], + splitting: false, + sourcemap: true, + clean: true, + dts: true, + legacyOutput: true, +})