diff --git a/config/tailwind.all_the_way.config.js b/config/tailwind.all_the_way.config.js index f5274cfed5f5f138495631ed3beba4bd1dee9c26..30f4ee5c876ce42ea286d0fd26e9dee81148751b 100644 --- a/config/tailwind.all_the_way.config.js +++ b/config/tailwind.all_the_way.config.js @@ -23,15 +23,9 @@ try { delete require.cache[path.resolve(__filename)]; } -const { content, ...remainingConfig } = tailwindGitLabDefaults; - /** @type {import('tailwindcss').Config} */ module.exports = { - ...remainingConfig, - content: [ - process.argv.includes('--only-used') ? 'false' : './config/helpers/tailwind/all_utilities.haml', - ...content, - ], + ...tailwindGitLabDefaults, corePlugins: { /* We set background: none, Tailwind background-image: none... diff --git a/package.json b/package.json index 68c8cd01c54699ea84b48ab35921ba662cc58119..dbf4bfd5032aa739b49d89b5094be8a222fb50e1 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "prejest": "yarn check-dependencies", "build:css": "node scripts/frontend/build_css.mjs", "tailwindcss:build": "node scripts/frontend/tailwindcss.mjs", - "pretailwindcss:build": "node scripts/frontend/tailwind_all_the_way.mjs", + "pretailwindcss:build": "node scripts/frontend/tailwind_all_the_way.mjs --only-used", "jest": "jest --config jest.config.js", "jest-debug": "node --inspect-brk node_modules/.bin/jest --runInBand", "jest:ci": "jest --config jest.config.js --ci --coverage --testSequencer ./scripts/frontend/parallel_ci_sequencer.js", diff --git a/scripts/frontend/tailwind_all_the_way.mjs b/scripts/frontend/tailwind_all_the_way.mjs index 6609569dfa67f6755f81f6443577d8802120bc8a..6bfadbdedf48607dcc428eae3ff15e16b7a92294 100755 --- a/scripts/frontend/tailwind_all_the_way.mjs +++ b/scripts/frontend/tailwind_all_the_way.mjs @@ -2,7 +2,7 @@ /* eslint-disable import/extensions */ -import fs from 'node:fs'; +import { mkdir, writeFile } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import path from 'node:path'; @@ -26,50 +26,26 @@ const ROOT_PATH = path.resolve(path.dirname(PATH_TO_FILE), '../../'); const tempDir = path.join(ROOT_PATH, 'config', 'helpers', 'tailwind'); const allUtilitiesFile = path.join(tempDir, './all_utilities.haml'); -export async function convertUtilsToCSSInJS() { - console.log('# Compiling legacy styles'); - - await compileAllStyles({ - style: 'expanded', - filter: (source) => source.includes('application_utilities_to_be_replaced'), +async function writeCssInJs(data) { + const formatted = await prettier.format(data, { + printWidth: 100, + singleQuote: true, + arrowParens: 'always', + trailingComma: 'all', + parser: 'babel', }); + return writeFile(path.join(tempDir, './css_in_js.js'), formatted, 'utf-8'); +} - fs.mkdirSync(tempDir, { recursive: true }); - - const oldUtilityDefinitions = extractRules( - loadCSSFromFile('app/assets/builds/application_utilities_to_be_replaced.css'), - { convertColors: true }, - ); - - // Write out all found css classes in order to run tailwind on it. - fs.writeFileSync( - allUtilitiesFile, - Object.keys(oldUtilityDefinitions) - .map((clazz) => { - return ( - // Add `gl-` prefix to all classes - `.gl-${clazz.substring(1)}` - // replace the escaped `\!` with ! - .replace(/\\!/g, '!') - ); - }) - .join('\n'), - ); - - // Lazily require - const { default: tailwindConfig } = await import('../../config/tailwind.all_the_way.config.js'); - - const { css: tailwindClasses } = await postcss([ - tailwindcss({ - ...tailwindConfig, - // We only want to generate the utils based on the fresh - // allUtilitiesFile - content: [allUtilitiesFile], - // We are disabling all plugins, so that the css-to-js - // import doesn't cause trouble. - plugins: [], - }), - ]).process('@tailwind utilities;', { map: false, from: undefined }); +/** + * 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 {string} tailwindClasses + * @param {Object} oldUtilityDefinitionsRaw + */ +async function toCompatibilityUtils(tailwindClasses, oldUtilityDefinitionsRaw) { + const oldUtilityDefinitions = _.clone(oldUtilityDefinitionsRaw); const tailwindDefinitions = extractRules(tailwindClasses); @@ -115,7 +91,7 @@ export async function convertUtilsToCSSInJS() { console.log(stats); - const output = await prettier.format( + await writeCssInJs( [ stats.potentialMismatches && ` @@ -153,18 +129,124 @@ const hardCodedColors = ${JSON.stringify(hardcodedColors, null, 2)}; }, ); - fs.writeFileSync(path.join(tempDir, './css_in_js.js'), output); + return stats; +} + +/** + * Writes only the style definitions we actually need. + */ +async function toMinimalUtilities() { + // We re-import the config with a `?minimal` query in order to cache-bust + // the previously loaded config, which doesn't have the latest css_in_js + const { default: tailwindConfig } = await import( + '../../config/tailwind.all_the_way.config.js?minimal' + ); + + const { css: tailwindClasses } = await postcss([ + tailwindcss({ + ...tailwindConfig, + // Disable all core plugins, all we care about are the legacy utils + // that are provided via addUtilities. + corePlugins: [], + }), + ]).process('@tailwind utilities;', { map: false, from: undefined }); + + const rules = extractRules(tailwindClasses); + + const minimalUtils = Object.keys(rules).length; - console.log('# Rebuilding tailwind-all-the-way'); + await writeCssInJs(` + /** + * The following ${minimalUtils} definitions need to be migrated to Tailwind. + * Let's do this! 🚀 + */ + module.exports = ${JSON.stringify(rules)}`); - await buildTailwind({ tailWindAllTheWay: true }); + return { minimalUtils }; +} + +/** + * To run the script in compatibility mode: + * + * ./scripts/frontend/tailwind_all_the_way.mjs + * + * This forces the generation of all possible utilities and surfaces the ones that might require + * further investigation. Once the output has been verified, the script can be re-run in minimal + * mode to only generate the utilities that are used in the product: + * + * ./scripts/frontend/tailwind_all_the_way.mjs --only-used + * + */ +export async function convertUtilsToCSSInJS({ buildOnlyUsed = false } = {}) { + console.log('# Compiling legacy styles'); + + await compileAllStyles({ + style: 'expanded', + filter: (source) => source.includes('application_utilities_to_be_replaced'), + }); + + await mkdir(tempDir, { recursive: true }); + + const oldUtilityDefinitions = extractRules( + loadCSSFromFile('app/assets/builds/application_utilities_to_be_replaced.css'), + { convertColors: true }, + ); + + // Write out all found css classes in order to run tailwind on it. + await writeFile( + allUtilitiesFile, + Object.keys(oldUtilityDefinitions) + .map((clazz) => { + return ( + // Add `gl-` prefix to all classes + `.gl-${clazz.substring(1)}` + // replace the escaped `\!` with ! + .replace(/\\!/g, '!') + ); + }) + .join('\n'), + 'utf-8', + ); + + // Lazily import the tailwind config + const { default: tailwindConfig } = await import( + '../../config/tailwind.all_the_way.config.js?default' + ); + + const { css: tailwindClasses } = await postcss([ + tailwindcss({ + ...tailwindConfig, + // We only want to generate the utils based on the fresh + // allUtilitiesFile + content: [allUtilitiesFile], + // We are disabling all plugins, so that the css-to-js + // import doesn't cause trouble. + plugins: [], + }), + ]).process('@tailwind utilities;', { map: false, from: undefined }); + + const stats = await toCompatibilityUtils(tailwindClasses, oldUtilityDefinitions); + + if (buildOnlyUsed) { + console.log('# Reducing utility definitions to minimally used'); + + const { minimalUtils } = await toMinimalUtilities(); + + console.log(`Went from ${stats.safeToUseLegacyUtils} => ${minimalUtils} utility classes`); + } + + await buildTailwind({ + tailWindAllTheWay: true, + content: buildOnlyUsed ? false : allUtilitiesFile, + }); return stats; } if (PATH_TO_FILE.includes(path.resolve(process.argv[1]))) { console.log('Script called directly.'); - convertUtilsToCSSInJS().catch((e) => { + console.log(`CWD${process.cwd()}`); + convertUtilsToCSSInJS({ buildOnlyUsed: process.argv.includes('--only-used') }).catch((e) => { console.warn(e); process.exitCode = 1; }); diff --git a/scripts/frontend/tailwindcss.mjs b/scripts/frontend/tailwindcss.mjs index 79e0cbdfa70f77240c385b9fae2ec89cce432de8..38b775f0b53fda051a167da79c82d04a041ed911 100644 --- a/scripts/frontend/tailwindcss.mjs +++ b/scripts/frontend/tailwindcss.mjs @@ -13,22 +13,38 @@ const ROOT_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '.. export async function build({ shouldWatch = false, tailWindAllTheWay = Boolean(process.env.TAILWIND_ALL_THE_WAY), + content = false, } = {}) { + const processorOptions = { + '--watch': shouldWatch, + '--output': path.join(ROOT_PATH, 'app/assets/builds', 'tailwind.css'), + '--input': path.join(ROOT_PATH, 'app/assets/stylesheets', 'tailwind.css'), + }; + let config = path.join(ROOT_PATH, 'config/tailwind.config.js'); - let fileName = 'tailwind.css'; if (tailWindAllTheWay) { + console.log('# Building "all the way" tailwind'); config = path.join(ROOT_PATH, 'config/tailwind.all_the_way.config.js'); - fileName = 'tailwind_all_the_way.css'; + processorOptions['--output'] = path.join( + ROOT_PATH, + 'app/assets/builds', + 'tailwind_all_the_way.css', + ); + processorOptions['--input'] = path.join( + ROOT_PATH, + 'app/assets/stylesheets', + 'tailwind_all_the_way.css', + ); + } else { + console.log('# Building "normal" tailwind'); } - const processor = await createProcessor( - { - '--watch': shouldWatch, - '--output': path.join(ROOT_PATH, 'app/assets/builds', fileName), - '--input': path.join(ROOT_PATH, 'app/assets/stylesheets', fileName), - }, - config, - ); + if (content) { + console.log(`Setting content to ${content}`); + processorOptions['--content'] = content; + } + + const processor = await createProcessor(processorOptions, config); if (shouldWatch) { return processor.watch();