From dc1f367e4f3aae4b4aa701813d1eb723481638b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Barth=C3=A9l=C3=A9my=20Ledoux?= Date: Mon, 22 Jul 2024 14:37:12 +0200 Subject: [PATCH 01/11] make components out of css classes for tailwind constants --- components/componentize.tailwind.rollup.mjs | 213 ++++++++++++++++++ components/const.rollup.config.mjs | 23 ++ package.json | 4 +- packages/component-tailwind-plugins/index.mjs | 30 +++ .../component-tailwind-plugins/package.json | 24 ++ ...ate-component-tailwind-plugins-package.mjs | 90 ++++++++ 6 files changed, 382 insertions(+), 2 deletions(-) create mode 100644 components/componentize.tailwind.rollup.mjs create mode 100644 packages/component-tailwind-plugins/index.mjs create mode 100644 packages/component-tailwind-plugins/package.json create mode 100644 scripts/generate-component-tailwind-plugins-package.mjs diff --git a/components/componentize.tailwind.rollup.mjs b/components/componentize.tailwind.rollup.mjs new file mode 100644 index 00000000..326c8077 --- /dev/null +++ b/components/componentize.tailwind.rollup.mjs @@ -0,0 +1,213 @@ +// @ts-check +import { parse } from '@babel/parser' +import t from '@babel/types' +import { visit, print } from 'recast' + +function parseCode(code) { + return parse(code, { + sourceType: 'module', + strictMode: false, + tokens: true, + plugins: [ + 'decorators-legacy', + 'doExpressions', + 'objectRestSpread', + 'classProperties', + 'classPrivateProperties', + 'classPrivateMethods', + 'exportDefaultFrom', + 'exportNamespaceFrom', + 'asyncGenerators', + 'functionBind', + 'functionSent', + 'dynamicImport', + 'numericSeparator', + 'optionalChaining', + 'importMeta', + 'bigInt', + 'optionalCatchBinding', + 'throwExpressions', + 'nullishCoalescingOperator', + 'importAssertions', + 'typescript', + ], + }) +} + +function visitConstantClassProperty(path, componentClassPrefix, onClass) { + for (const property of path.properties) { + if (property.type === 'ObjectProperty') { + const { value, key } = property + const keyName = + key.type === 'Identifier' + ? key.name + : key.type === 'StringLiteral' + ? key.value + : null + + if (!keyName) { + throw Error(`wrong key type ${key.type}`) + } + + if (value.type === 'StringLiteral') { + onClass( + makeClassName(`${componentClassPrefix}-${keyName}`), + value.value, + value, + ) + } else if (value.type === 'ObjectExpression') { + visitConstantClassProperty( + value, + `${componentClassPrefix}-${keyName}`, + onClass, + ) + } + } + } +} + +function consolidateBinaryExpression(path) { + const { left, right, operator } = path + if (operator !== '+') { + throw Error(`unsupported operator ${operator}`) + } + + if (left.type === 'BinaryExpression') { + return `${consolidateBinaryExpression(left)}${right.value}` + } + return `${left.value}${right.value}` +} + +function visitConstantClass(path, componentClassPrefix, onClass) { + // if the export is a constant declaration and the name of the constant starts with css + // then replace the values with some component classes + const { + node: { declaration: topDeclaration }, + } = path + if (topDeclaration && topDeclaration.type === 'VariableDeclaration') { + for (const declaration of topDeclaration.declarations) { + if (declaration.type !== 'VariableDeclarator') return false + const { id, init } = declaration + if (id?.type === 'Identifier') { + const varName = id.name.startsWith('Css') + ? id.name.slice(3).toLowerCase() + : id.name.startsWith('Class') + ? id.name.slice(5).toLowerCase() + : null + if (!varName) return false + + const pathToVisit = + init?.type === 'TSAsExpression' ? init.expression : init + + if (pathToVisit.type === 'StringLiteral') { + onClass( + makeClassName(`${componentClassPrefix}-${varName}`), + pathToVisit.value, + pathToVisit, + ) + } + + if (pathToVisit.type === 'BinaryExpression') { + const binaryExpressionString = + consolidateBinaryExpression(pathToVisit) + + declaration.init = t.stringLiteral(binaryExpressionString) + + onClass( + makeClassName(`${componentClassPrefix}-${varName}`), + binaryExpressionString, + declaration.init, + ) + } + + if (pathToVisit.type === 'ObjectExpression') { + visitConstantClassProperty( + pathToVisit, + `${componentClassPrefix}-${varName}`, + onClass, + ) + } + } + } + } +} + +function makeClassName(className) { + return className.toLowerCase().replace(/-classes-/g, '-') +} + +/** + * @type {() => import('rollup').Plugin} + */ +export function MakeTailwindComponentPluginRollupPlugin() { + return { + name: 'cypress-design:make-tailwind-component', + transform(code, id) { + const pathArray = id.split('/') + + const componentClassPrefix = `cyds-${pathArray[pathArray.indexOf('constants') - 1].toLowerCase()}` + const ast = parseCode(code) + + const componentClassesDefinitions = {} + + visit(ast, { + visitExportNamedDeclaration(path) { + visitConstantClass(path, componentClassPrefix, (key, value) => { + if (value.length && !key.includes('icon')) { + componentClassesDefinitions[`.${key}`] = { + [`@apply ${value}`]: {}, + } + } + }) + return false + }, + }) + + return { + code: `export default function plugin({ addComponents }) { + ${ + Object.keys(componentClassesDefinitions).length + ? `addComponents(${JSON.stringify(componentClassesDefinitions, null, 2)})` + : '' + } +}`, + } + }, + } +} + +/** + * @type {() => import('rollup').Plugin} + */ +export function ComponentClassesRollupPlugin() { + return { + name: 'cypress-design:component-classes', + transform(code, id) { + const pathArray = id.split('/') + + const componentName = pathArray[pathArray.indexOf('constants') - 1] + + if (!componentName) { + return { code } + } + + const componentClassPrefix = `cyds-${componentName.toLowerCase()}` + const ast = parseCode(code) + + visit(ast, { + visitExportNamedDeclaration(path) { + visitConstantClass(path, componentClassPrefix, (key, value, init) => { + if (value.length && !key.includes('icon')) { + init.value = key + } + }) + return false + }, + }) + + return { + code: print(ast).code, + } + }, + } +} diff --git a/components/const.rollup.config.mjs b/components/const.rollup.config.mjs index c595fc0e..f48f7c8c 100644 --- a/components/const.rollup.config.mjs +++ b/components/const.rollup.config.mjs @@ -2,6 +2,10 @@ import commonjs from '@rollup/plugin-commonjs' import resolve from '@rollup/plugin-node-resolve' import typescript from '@rollup/plugin-typescript' +import { + MakeTailwindComponentPluginRollupPlugin, + ComponentClassesRollupPlugin, +} from './componentize.tailwind.rollup.mjs' export default ({ input = './src/index.ts', plugins = [], external = [] }) => [ { @@ -26,8 +30,27 @@ export default ({ input = './src/index.ts', plugins = [], external = [] }) => [ tsconfig: './tsconfig.json', declarationMap: true, }), + ComponentClassesRollupPlugin(), ...plugins, ], external, }, + { + input, + output: [ + { + file: './dist/tailwind-plugin.es.mjs', + format: 'esm', + sourcemap: true, + }, + ], + plugins: [ + typescript({ + tsconfig: './tsconfig.json', + noEmit: true, + }), + MakeTailwindComponentPluginRollupPlugin(), + ], + external, + }, ] diff --git a/package.json b/package.json index 87315b4d..fdabd7bb 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "cy:run": "cypress run --component", "test": "vitest", "test:eslint-plugin": "yarn workspace @cypress-design/eslint-plugin run test", - "build:components": "turbo run --no-daemon build --filter=\"@cypress-design/constants-*\" && turbo run --no-daemon build --filter=\"@cypress-design/vue-*\" --filter=\"@cypress-design/react-*\"", + "build:components": "turbo run --no-daemon build --filter=\"@cypress-design/constants-*\" && node ./scripts/generate-component-tailwind-plugins-package.mjs && turbo run --no-daemon build --filter=\"@cypress-design/vue-*\" --filter=\"@cypress-design/react-*\"", "dev": "yarn start", "docs": "yarn start", "start": "NODE_ENV=development concurrently -n const,icons,docgen,vitepress,cleanup \"node ./scripts/watch-constants.mjs\" \"node ./scripts/watch-icons.mjs\" \"vue-docgen --watch -c docs/docgen/docgen.config.cjs\" \"vitepress dev docs\" \"node ./scripts/clean-component-docs.mjs\"", @@ -119,4 +119,4 @@ "pre-commit": "lint-staged" }, "packageManager": "yarn@1.22.22" -} +} \ No newline at end of file diff --git a/packages/component-tailwind-plugins/index.mjs b/packages/component-tailwind-plugins/index.mjs new file mode 100644 index 00000000..000cc9af --- /dev/null +++ b/packages/component-tailwind-plugins/index.mjs @@ -0,0 +1,30 @@ +import plugin from 'tailwindcss/plugin' +import Accordion from '@cypress-design/constants-accordion/tailwind' +import Alert from '@cypress-design/constants-alert/tailwind' +import Button from '@cypress-design/constants-button/tailwind' +import Checkbox from '@cypress-design/constants-checkbox/tailwind' +import DocMenu from '@cypress-design/constants-docmenu/tailwind' +import Menu from '@cypress-design/constants-menu/tailwind' +import Modal from '@cypress-design/constants-modal/tailwind' +import Spinner from '@cypress-design/constants-spinner/tailwind' +import StatusIcon from '@cypress-design/constants-statusicon/tailwind' +import Tabs from '@cypress-design/constants-tabs/tailwind' +import Tag from '@cypress-design/constants-tag/tailwind' +import TestResult from '@cypress-design/constants-testresult/tailwind' + +const componentPlugins = [ + plugin(Accordion), + plugin(Alert), + plugin(Button), + plugin(Checkbox), + plugin(DocMenu), + plugin(Menu), + plugin(Modal), + plugin(Spinner), + plugin(StatusIcon), + plugin(Tabs), + plugin(Tag), + plugin(TestResult) +] + +export default componentPlugins \ No newline at end of file diff --git a/packages/component-tailwind-plugins/package.json b/packages/component-tailwind-plugins/package.json new file mode 100644 index 00000000..784288ca --- /dev/null +++ b/packages/component-tailwind-plugins/package.json @@ -0,0 +1,24 @@ +{ + "name": "@cypress-design/component-tailwind-plugins", + "version": "0.0.0", + "private": "true", + "exports": { + "import": "./index.mjs", + "types": "./dist/index.d.ts" + }, + "devDependencies": { + "@cypress-design/constants-accordion": "*", + "@cypress-design/constants-alert": "*", + "@cypress-design/constants-button": "*", + "@cypress-design/constants-checkbox": "*", + "@cypress-design/constants-docmenu": "*", + "@cypress-design/constants-menu": "*", + "@cypress-design/constants-modal": "*", + "@cypress-design/constants-spinner": "*", + "@cypress-design/constants-statusicon": "*", + "@cypress-design/constants-tabs": "*", + "@cypress-design/constants-tag": "*", + "@cypress-design/constants-testresult": "*", + "tailwindcss": "^3.4.3" + } +} \ No newline at end of file diff --git a/scripts/generate-component-tailwind-plugins-package.mjs b/scripts/generate-component-tailwind-plugins-package.mjs new file mode 100644 index 00000000..ad8125ba --- /dev/null +++ b/scripts/generate-component-tailwind-plugins-package.mjs @@ -0,0 +1,90 @@ +import { resolve } from 'path' +import { promises as fs } from 'fs' +import { globby } from 'globby' +import * as url from 'url' + +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) + +async function createPackages() { + const constantsPackages = await globby( + './components/*/constants/package.json', + { + cwd: resolve(__dirname, '../'), + }, + ) + + const constantsPackagesWithTailwindPlugin = [] + + for (const pkgPath of constantsPackages) { + const { + default: { exports }, + } = await import(resolve(__dirname, '../', pkgPath), { + assert: { type: 'json' }, + }) + + // check if the exports?.['./tailwind'] exists and if the targeted file actually exists + if (exports?.['./tailwind']) { + let hasTailwind = true + const pluginPath = resolve( + __dirname, + '../', + pkgPath.replace('package.json', 'dist/tailwind-plugin.es.mjs'), + ) + try { + await fs.access(pluginPath) + } catch { + hasTailwind = false + } + if (hasTailwind) { + // add current package to the list of tailwind plugins + const pathArray = pkgPath.split('/') + constantsPackagesWithTailwindPlugin.push( + pathArray[pathArray.indexOf('package.json') - 2], + ) + } + } + } + + constantsPackagesWithTailwindPlugin.sort() + return constantsPackagesWithTailwindPlugin +} + +async function generate() { + const pkgs = await createPackages() + const packageFolder = resolve( + resolve(__dirname, '../packages/component-tailwind-plugins'), + ) + await fs.mkdir(packageFolder, { recursive: true }) + await fs.writeFile( + resolve(packageFolder, './index.mjs'), + `import plugin from 'tailwindcss/plugin' +${pkgs.map((p) => `import ${p.replace(/-/g, '_')} from '@cypress-design/constants-${p.toLowerCase()}/tailwind'`).join('\n')} + +const componentPlugins = [ + ${pkgs.map((p) => `plugin(${p.replace(/-/g, '_')})`).join(',\n\t')} +] + +export default componentPlugins`, + { encoding: 'utf-8' }, + ) + + await fs.writeFile( + resolve(__dirname, '../packages/component-tailwind-plugins/package.json'), + `{ + "name": "@cypress-design/component-tailwind-plugins", + "version": "0.0.0", + "private": "true", + "exports": { + "import": "./index.mjs", + "types": "./dist/index.d.ts" + }, + "devDependencies": { + ${pkgs.map((p) => `"@cypress-design/constants-${p.toLowerCase()}": "*",`).join('\n\t\t')} + "tailwindcss": "^3.4.3" + } +}`, + { encoding: 'utf-8' }, + ) +} + +generate() From 9706e851f4eb3e04f0b2243d8f0aab84ac00bf3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Barth=C3=A9l=C3=A9my=20Ledoux?= Date: Mon, 22 Jul 2024 16:50:27 +0200 Subject: [PATCH 02/11] add components to css packages --- css/package.json | 1 + css/src/tailwind.config.ts | 2 ++ packages/component-tailwind-plugins/index.d.ts | 3 +++ packages/component-tailwind-plugins/package.json | 2 +- .../generate-component-tailwind-plugins-package.mjs | 12 ++++++++++-- 5 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 packages/component-tailwind-plugins/index.d.ts diff --git a/css/package.json b/css/package.json index 82db68d6..73f26629 100644 --- a/css/package.json +++ b/css/package.json @@ -42,6 +42,7 @@ }, "devDependencies": { "@cypress-design/color-constants": "*", + "@cypress-design/component-tailwind-plugins": "*", "@types/lodash": "^4.17.4", "typescript": "5.4.5", "color-string": "1.9.1" diff --git a/css/src/tailwind.config.ts b/css/src/tailwind.config.ts index 1ebcba50..998289fb 100644 --- a/css/src/tailwind.config.ts +++ b/css/src/tailwind.config.ts @@ -9,6 +9,7 @@ import detailsOpenVariantPlugin from './tw-details-open-variant-plugin' import plugin from 'tailwindcss/plugin' import containerPlugin from '@tailwindcss/container-queries' import Hocus from 'tailwindcss-hocus' +import componentPlugins from '@cypress-design/component-tailwind-plugins' function defineConfig(config: Config) { return config @@ -71,6 +72,7 @@ export default (fileGlobs: string[] = []) => { }, }) }), + ...componentPlugins, ], }) } diff --git a/packages/component-tailwind-plugins/index.d.ts b/packages/component-tailwind-plugins/index.d.ts new file mode 100644 index 00000000..d9d5b83a --- /dev/null +++ b/packages/component-tailwind-plugins/index.d.ts @@ -0,0 +1,3 @@ +import type { Plugin } from 'tailwindcss/plugin' +const componentPlugins: Plugin[] +export default componentPlugins diff --git a/packages/component-tailwind-plugins/package.json b/packages/component-tailwind-plugins/package.json index 784288ca..59f73bc7 100644 --- a/packages/component-tailwind-plugins/package.json +++ b/packages/component-tailwind-plugins/package.json @@ -4,7 +4,7 @@ "private": "true", "exports": { "import": "./index.mjs", - "types": "./dist/index.d.ts" + "types": "./index.d.ts" }, "devDependencies": { "@cypress-design/constants-accordion": "*", diff --git a/scripts/generate-component-tailwind-plugins-package.mjs b/scripts/generate-component-tailwind-plugins-package.mjs index ad8125ba..87d4091e 100644 --- a/scripts/generate-component-tailwind-plugins-package.mjs +++ b/scripts/generate-component-tailwind-plugins-package.mjs @@ -69,14 +69,22 @@ export default componentPlugins`, ) await fs.writeFile( - resolve(__dirname, '../packages/component-tailwind-plugins/package.json'), + resolve(packageFolder, './index.d.ts'), + `import type { Plugin } from 'tailwindcss/plugin' + const componentPlugins: Plugin[] + export default componentPlugins`, + { encoding: 'utf-8' }, + ) + + await fs.writeFile( + resolve(packageFolder, './package.json'), `{ "name": "@cypress-design/component-tailwind-plugins", "version": "0.0.0", "private": "true", "exports": { "import": "./index.mjs", - "types": "./dist/index.d.ts" + "types": "./index.d.ts" }, "devDependencies": { ${pkgs.map((p) => `"@cypress-design/constants-${p.toLowerCase()}": "*",`).join('\n\t\t')} From a4f1d0bd8450c4f9ae88773bc742d8368d28c5ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Barth=C3=A9l=C3=A9my=20Ledoux?= Date: Mon, 22 Jul 2024 16:51:49 +0200 Subject: [PATCH 03/11] dedent --- scripts/generate-component-tailwind-plugins-package.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/generate-component-tailwind-plugins-package.mjs b/scripts/generate-component-tailwind-plugins-package.mjs index 87d4091e..e3783768 100644 --- a/scripts/generate-component-tailwind-plugins-package.mjs +++ b/scripts/generate-component-tailwind-plugins-package.mjs @@ -71,8 +71,8 @@ export default componentPlugins`, await fs.writeFile( resolve(packageFolder, './index.d.ts'), `import type { Plugin } from 'tailwindcss/plugin' - const componentPlugins: Plugin[] - export default componentPlugins`, +const componentPlugins: Plugin[] +export default componentPlugins`, { encoding: 'utf-8' }, ) From 95bd653cb70693dd22ed5bf58381200058c3e2b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Barth=C3=A9l=C3=A9my=20Ledoux?= Date: Mon, 22 Jul 2024 16:52:28 +0200 Subject: [PATCH 04/11] lint stuff --- scripts/generate-component-tailwind-plugins-package.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/generate-component-tailwind-plugins-package.mjs b/scripts/generate-component-tailwind-plugins-package.mjs index e3783768..e668067c 100644 --- a/scripts/generate-component-tailwind-plugins-package.mjs +++ b/scripts/generate-component-tailwind-plugins-package.mjs @@ -72,7 +72,8 @@ export default componentPlugins`, resolve(packageFolder, './index.d.ts'), `import type { Plugin } from 'tailwindcss/plugin' const componentPlugins: Plugin[] -export default componentPlugins`, +export default componentPlugins +`, { encoding: 'utf-8' }, ) From 2629d5e6b07225f396eb986d241b5f6c3352dbcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Barth=C3=A9l=C3=A9my=20Ledoux?= Date: Mon, 22 Jul 2024 20:14:42 +0200 Subject: [PATCH 05/11] fix: doc link css --- components/DocMenu/constants/src/index.ts | 11 ++++++++ components/DocMenu/react/_DocLink.tsx | 28 ++++++++----------- components/DocMenu/vue/_DocLink.vue | 34 +++++++++++++---------- 3 files changed, 42 insertions(+), 31 deletions(-) diff --git a/components/DocMenu/constants/src/index.ts b/components/DocMenu/constants/src/index.ts index 0646f6bd..5409bc61 100644 --- a/components/DocMenu/constants/src/index.ts +++ b/components/DocMenu/constants/src/index.ts @@ -22,3 +22,14 @@ export const CssClasses = { openListBorderLeft: 'absolute left-[8px] top-0 w-[1px] h-full bg-gray-100 dark:bg-gray-900', } as const + +export const CssLink = { + wrapper: 'list-none p-0 scroll-my-10', + static: 'group relative block w-full pl-[24px]', + active: 'text-indigo-500 dark:text-indigo-400', + inactive: 'text-gray-700 dark:text-gray-500', + negativeDepth: 'py-[8px] text-[16px] leading-[24px]', + positiveDepth: 'leading-[20px] text-[14px] py-[12px]', + markerStatic: + 'left-[6.5px] absolute top-[4px] bottom-[4px] w-[4px] z-10 rounded-full', +} as const diff --git a/components/DocMenu/react/_DocLink.tsx b/components/DocMenu/react/_DocLink.tsx index c1c5bacb..b469c066 100644 --- a/components/DocMenu/react/_DocLink.tsx +++ b/components/DocMenu/react/_DocLink.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import clsx from 'clsx' -import { NavItemLink } from '@cypress-design/constants-docmenu' +import { type NavItemLink, CssLink } from '@cypress-design/constants-docmenu' export interface Context { setMarkerIsMoving: (markerIsMoving: boolean) => void @@ -86,14 +86,14 @@ export const DocLink = React.forwardRef( })) return ( -
  • +
  • = 0, + className={clsx(CssLink.static, { + [CssLink.active]: active, + [CssLink.inactive]: !active, + [CssLink.negativeDepth]: depth < 0, + [CssLink.positiveDepth]: depth >= 0, })} style={{ paddingLeft: depth >= 0 ? `${depth * 12 + 48}px` : undefined, @@ -101,15 +101,11 @@ export const DocLink = React.forwardRef( > {depth >= 0 ? (
    ) : null} {item.label} diff --git a/components/DocMenu/vue/_DocLink.vue b/components/DocMenu/vue/_DocLink.vue index 462a0099..0857e68a 100644 --- a/components/DocMenu/vue/_DocLink.vue +++ b/components/DocMenu/vue/_DocLink.vue @@ -8,7 +8,7 @@ import { nextTick, computed, } from 'vue' -import type { NavItemLink } from '@cypress-design/constants-docmenu' +import { type NavItemLink, CssLink } from '@cypress-design/constants-docmenu' const props = withDefaults( defineProps<{ @@ -102,29 +102,33 @@ const itemWithoutLabel = computed(() => {