diff --git a/package.json b/package.json index f5839d44e66814f2a742578cd4943d4cddfb55a6..8eb598f8feb78cc9ff7772838bf197b50e747dce 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "lint:stylelint:fix": "yarn run lint:stylelint --fix", "lint:stylelint:staged": "scripts/frontend/execute-on-staged-files.sh stylelint '(css|scss)' -q", "lint:stylelint:staged:fix": "yarn run lint:stylelint:staged --fix", - "lint:tailwind-utils": "REDIRECT_TO_STDOUT=true node scripts/frontend/tailwind_lint_against_legacy_utils.js", "markdownlint": "markdownlint-cli2", "preinstall": "node ./scripts/frontend/preinstall.mjs", "postinstall": "node ./scripts/frontend/postinstall.js", @@ -76,7 +75,7 @@ "@gitlab/fonts": "^1.3.0", "@gitlab/query-language": "^0.0.5-a-20241017", "@gitlab/svgs": "3.119.0", - "@gitlab/ui": "98.5.2", + "@gitlab/ui": "99.0.0", "@gitlab/web-ide": "^0.0.1-dev-20240909013227", "@mattiasbuelens/web-streams-adapter": "^0.1.0", "@rails/actioncable": "7.0.8-4", diff --git a/scripts/allowed_warnings.txt b/scripts/allowed_warnings.txt index b8f3483159505cc43dfe4fd2e31d762ca09d2ce3..32f0ac8e7a0f7a3d2735934dc67813cd2a753eb7 100644 --- a/scripts/allowed_warnings.txt +++ b/scripts/allowed_warnings.txt @@ -31,11 +31,5 @@ warning: IO::Buffer is experimental and both the Ruby and C interface may change Rebuilding... Done in [0-9]+ms. -# We are in the midst of legacy util to Tailwind migration and for linting purposes we are checking -# whether someone uses legacy utils. If no legacy utils are found, tailwind sends a warning -# This can be removed once we do not compile legacy utils any longer -.+ - No utility classes were detected in your source files. If this is unexpected, double-check the `content` option in your Tailwind CSS configuration. -.+ - https://tailwindcss.com/docs/content-configuration - # The next major version of the view_component gem will require Rails >= 7.1 Support for Rails versions < 7.1 is deprecated and will be removed from ViewComponent 4.0.0 diff --git a/scripts/frontend/tailwind_lint_against_legacy_utils.js b/scripts/frontend/tailwind_lint_against_legacy_utils.js deleted file mode 100755 index 99f903896bbf03a5a7a0994ae1d8372ba75896ba..0000000000000000000000000000000000000000 --- a/scripts/frontend/tailwind_lint_against_legacy_utils.js +++ /dev/null @@ -1,220 +0,0 @@ -#!/usr/bin/env node - -/* eslint-disable import/extensions */ - -const { readFile } = require('node:fs/promises'); -const path = require('node:path'); -const _ = require('lodash'); -const postcss = require('postcss'); -const tailwindPlugin = require('tailwindcss/plugin.js'); -const tailwindcss = require('tailwindcss/lib/plugin.js'); -const tailwindConfig = require('../../config/tailwind.config.js'); - -const ROOT_PATH = path.resolve(__dirname, '../../'); -const tailwindSource = path.join(ROOT_PATH, 'app/assets/stylesheets/tailwind.css'); -const legacyUtilsSource = path.join(ROOT_PATH, 'node_modules/@gitlab/ui/dist/utility_classes.css'); - -/** - * Strips trailing modifiers like `:hover`, `::before` from selectors, - * only returning the class name - * - * For example: .gl-foo-hover-bar:hover will be turned into: .gl-foo-bar - * @param {String} selector - * @returns {String} - */ -function getCleanSelector(selector) { - return selector.replace(/:.*/, ''); -} - -/** - * Extracts all class names from a CSS file - * - * @param {String} css - * @returns {Set<String>} - */ -function extractClassNames(css) { - const definitions = new Set(); - - postcss.parse(css).walkRules((rule) => { - // We skip all atrule, e.g. @keyframe, except @media queries - if (rule.parent?.type === 'atrule' && rule.parent?.name !== 'media') { - console.log(`Skipping atrule of type ${rule.parent?.name}`); - return; - } - - // This is an odd dark-mode only util. We have added it to the dark mode overrides - // and remove it from our utility classes - if (rule.selector.startsWith('.gl-dark .gl-dark-invert-keep-hue')) { - console.log(`Skipping composite selector ${rule.selector} which will be migrated manually`); - return; - } - - // iterate over each class definition - rule.selectors.forEach((selector) => { - definitions.add(getCleanSelector(selector)); - }); - }); - - return definitions; -} - -/** - * Writes the CSS in Js in compatibility mode. We write all the utils and we surface things we might - * want to look into (hardcoded colors, definition mismatches). - * - * @param {Set<String>} tailwindClassNames - * @param {Set<String>} oldClassNames - */ -async function compareLegacyClassesWithTailwind(tailwindClassNames, oldClassNames) { - const oldUtilityNames = new Set(oldClassNames); - - const deleted = new Set(); - - for (const definition of tailwindClassNames) { - if (oldUtilityNames.has(definition)) { - oldUtilityNames.delete(definition); - deleted.add(definition); - } - } - - console.log( - `Legacy classes which have a tailwind equivalent:\n\t${_.chunk(Array.from(deleted), 4) - .map((n) => n.join(' ')) - .join('\n\t')}`, - ); - - return { oldUtilityNames }; -} - -/** - * Runs tailwind on the whole code base, but with mock utilities only. - * - * We hand in a Set of class names (e.g. `.foo-bar`, `.bar-baz`) and tailwind will run - * if one of our source files contains e.g. `.gl-foo-bar` or `.gl-bar-baz`, - * it will be returned - * - * @param {Set<String>} oldClassNames - * @param {Array<string>} content - * @returns {Promise<{rules: Set<String>}>} - */ -async function toMinimalUtilities(oldClassNames, content = []) { - const { css: tailwindClasses } = await postcss([ - tailwindcss({ - ...tailwindConfig, - content: Array.isArray(content) && content.length > 0 ? content : tailwindConfig.content, - // We must ensure the GitLab UI plugin is disabled during this run so that whatever it defines - // is purged out of the CSS-in-Js. - presets: [ - { - ...tailwindConfig.presets[0], - plugins: [], - }, - ], - // Disable all core plugins, all we care about are the legacy utils - // that are provided via addUtilities. - corePlugins: [], - plugins: [ - tailwindPlugin(({ addUtilities }) => { - addUtilities( - Object.fromEntries( - Array.from(oldClassNames).map((className) => [ - // Strip leading `.gl-` because tailwind will add the prefix itself - className.replace(/^\.gl-/, '.'), - { width: 0 }, - ]), - ), - ); - }), - ], - }), - ]).process('@tailwind utilities;', { map: false, from: undefined }); - - const rules = tailwindClasses - .replace(/@.+?{([\s\S]+?)}/gim, '$1') - .replace(/\{[\s\S]+?}/gim, '') - .split('\n') - .map((x) => x.trim()) - .filter(Boolean); - - return { rules: new Set(rules) }; -} - -async function lintAgainstLegacyUtils({ content = [] } = {}) { - console.log('# Checking whether legacy GitLab utility classes are used'); - - console.log('## Extracting legacy util class names'); - - const legacyClassNames = extractClassNames(await readFile(legacyUtilsSource, 'utf-8')); - - /** - * Document containing all utilities at least once, like this: - * - * <div class="gl-display-flex"> - * <div class="gl-foo-bar"> - * @type {string} - */ - const allLegacyDocument = Array.from(legacyClassNames) - .map((className) => { - const cleanClass = className - .substring(1) - // replace escaped `\!` with ! - .replace(/\\!/g, '!'); - - return `<div class="${cleanClass}"></div>`; - }) - .join('\n'); - - const { css } = await postcss([ - tailwindcss({ - ...tailwindConfig, - content: [{ raw: allLegacyDocument, extension: 'html' }], - // We are disabling all plugins to prevent the CSS-in-Js import from causing trouble. - // The GitLab UI preset still registers its own plugin, which we need to define legitimate - // custom utils. - plugins: [], - }), - ]).process(await readFile(tailwindSource, 'utf-8'), { map: false, from: undefined }); - - const tailwindClassNames = extractClassNames(css); - - console.log('## Comparing legacy utils to current tailwind class names'); - - const { oldUtilityNames } = await compareLegacyClassesWithTailwind( - tailwindClassNames, - legacyClassNames, - ); - - console.log('## Checking whether a legacy class name is used'); - - const { rules } = await toMinimalUtilities(oldUtilityNames, content); - - console.log(`Went from ${oldUtilityNames.size} => ${rules.size} utility classes`); - - if (rules.size > 0) { - const message = `You are introducing legacy utilities: -\t${Array.from(rules).sort().join('\n\t')} -Please migrate them to tailwind utilities: -https://gitlab.com/gitlab-org/gitlab-ui/-/blob/main/doc/tailwind-migration.md`; - throw new Error(message); - } -} - -function wasScriptCalledDirectly() { - return process.argv[1] === __filename; -} - -if (wasScriptCalledDirectly()) { - lintAgainstLegacyUtils() - .then(() => { - console.log('# All good – Happiness. May the tailwind boost your journey'); - }) - .catch((e) => { - console.warn('An error happened'); - console.warn(e.message); - process.exitCode = 1; - }); -} - -module.exports = { - lintAgainstLegacyUtils, -}; diff --git a/scripts/static-analysis b/scripts/static-analysis index 57d19dfdc797834b467fc1a66e38a3a8a3fa9b6a..fdfd988d839a986308826e211db728afc64bb021 100755 --- a/scripts/static-analysis +++ b/scripts/static-analysis @@ -43,7 +43,6 @@ class StaticAnalysis Task.new(%W[scripts/license-check.sh #{project_path}], 200), (Gitlab.ee? ? Task.new(%w[bin/rake gettext:updated_check], 40) : nil), Task.new(%w[bin/rake lint:static_verification], 40), - Task.new(%w[yarn run lint:tailwind-utils], 20), Task.new(%w[bin/rake config_lint], 10), Task.new(%w[bin/rake gitlab:sidekiq:all_queues_yml:check], 15), (Gitlab.ee? ? Task.new(%w[bin/rake gitlab:sidekiq:sidekiq_queues_yml:check], 11) : nil), diff --git a/spec/frontend/scripts/frontend/tailwind_lint_against_legacy_utils_spec.js b/spec/frontend/scripts/frontend/tailwind_lint_against_legacy_utils_spec.js deleted file mode 100644 index d95df0d153be99c7190baeab5fbe2a48d74a16dc..0000000000000000000000000000000000000000 --- a/spec/frontend/scripts/frontend/tailwind_lint_against_legacy_utils_spec.js +++ /dev/null @@ -1,40 +0,0 @@ -import { lintAgainstLegacyUtils } from '../../../../scripts/frontend/tailwind_lint_against_legacy_utils'; - -describe('lintAgainstLegacyUtils', () => { - beforeEach(() => { - jest.spyOn(console, 'log').mockImplementation(jest.fn()); - jest.spyOn(console, 'warn').mockImplementation(jest.fn()); - }); - - it('does not throw if no legacy utils are used', async () => { - await expect( - lintAgainstLegacyUtils({ content: [{ raw: '<div class="gl-block">', extension: 'html' }] }), - ).resolves.not.toThrow(); - }); - - describe('legacy utils are used', () => { - it('does throw on basic legacy utils', async () => { - await expect( - lintAgainstLegacyUtils({ - content: [{ raw: '<div class="gl-display-block">', extension: 'html' }], - }), - ).rejects.toThrow(/You are introducing legacy utilities[\s\S]+\.gl-display-block/gm); - }); - - it('does throw with modified legacy utils', async () => { - await expect( - lintAgainstLegacyUtils({ - content: [{ raw: '<div class="md:gl-display-block">', extension: 'html' }], - }), - ).rejects.toThrow(/You are introducing legacy utilities[\s\S]+\.md\\:gl-display-block/gm); - }); - - it('does throw with important legacy utils', async () => { - await expect( - lintAgainstLegacyUtils({ - content: [{ raw: '<div class="!gl-display-block">', extension: 'html' }], - }), - ).rejects.toThrow(/You are introducing legacy utilities[\s\S]+\.\\!gl-display-block/gm); - }); - }); -}); diff --git a/yarn.lock b/yarn.lock index 6a6afc7c07990815dde38c4ec58caa73368b832e..7b15443a6cac7857cd72ac4044ca1afade5bee00 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1401,10 +1401,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.119.0.tgz#becbeea7e7ee241baecdad02b9ad04de7a8aed04" integrity sha512-Os/PF37pCY75uLA0dmGaZe13BmirzlWH+pFLinCAPRChEC7KhHCJtIy0efRAxzkA4uatmHpJHxftuTc7NeiSNQ== -"@gitlab/ui@98.5.2": - version "98.5.2" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-98.5.2.tgz#7dd8b670c474c3d7e6bf910955c1a3695ffc7496" - integrity sha512-5FQgkHat0nIjjxLlygSlhIMSS0hE4VPYOY+WSk4XEQzt9Cqgl/GXsPyjxDMw7o8f90pkCUNNoEiEmf0mZ8bF5w== +"@gitlab/ui@99.0.0": + version "99.0.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-99.0.0.tgz#068f4defb9fe80264d0bd54e32df7b25892b710b" + integrity sha512-f5g4dk43ZjqjxSGee44JvB4Nh9m0GybcFEMtLwvgoKQl60oEdLD7hQZv/fh1+IVcXbp0RgOEyy3wsSW9/iItKg== dependencies: "@floating-ui/dom" "1.4.3" echarts "^5.3.2" @@ -13776,16 +13776,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -13838,7 +13829,7 @@ string_decoder@^1.0.0, string_decoder@^1.1.1, string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -13852,13 +13843,6 @@ strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -15583,7 +15567,7 @@ worker-loader@^3.0.8: loader-utils "^2.0.0" schema-utils "^3.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -15601,15 +15585,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"