diff options
Diffstat (limited to 'scripts/frontend/startup_css')
-rw-r--r-- | scripts/frontend/startup_css/clean_css.js | 83 | ||||
-rw-r--r-- | scripts/frontend/startup_css/constants.js | 106 | ||||
-rw-r--r-- | scripts/frontend/startup_css/get_css_path.js | 22 | ||||
-rw-r--r-- | scripts/frontend/startup_css/get_startup_css.js | 69 | ||||
-rw-r--r-- | scripts/frontend/startup_css/main.js | 60 | ||||
-rwxr-xr-x | scripts/frontend/startup_css/setup.sh | 76 | ||||
-rwxr-xr-x | scripts/frontend/startup_css/startup_css_changed.sh | 40 | ||||
-rw-r--r-- | scripts/frontend/startup_css/utils.js | 8 | ||||
-rw-r--r-- | scripts/frontend/startup_css/write_startup_scss.js | 28 |
9 files changed, 492 insertions, 0 deletions
diff --git a/scripts/frontend/startup_css/clean_css.js b/scripts/frontend/startup_css/clean_css.js new file mode 100644 index 00000000000..67a0453e816 --- /dev/null +++ b/scripts/frontend/startup_css/clean_css.js @@ -0,0 +1,83 @@ +const { memoize, isString, isRegExp } = require('lodash'); +const { parse } = require('postcss'); +const { CSS_TO_REMOVE } = require('./constants'); + +const getSelectorRemoveTesters = memoize(() => + CSS_TO_REMOVE.map((x) => { + if (isString(x)) { + return (selector) => x === selector; + } + if (isRegExp(x)) { + return (selector) => x.test(selector); + } + + throw new Error(`Unexpected type in CSS_TO_REMOVE content "${x}". Expected String or RegExp.`); + }), +); + +const getRemoveTesters = memoize(() => { + const selectorTesters = getSelectorRemoveTesters(); + + // These are mostly carried over from the previous project + // https://gitlab.com/gitlab-org/frontend/gitlab-css-statistics/-/blob/2aa00af25dba08fc71081c77206f45efe817ea4b/lib/gl_startup_extract.js + return [ + (node) => node.type === 'comment', + (node) => + node.type === 'atrule' && + (node.params === 'print' || + node.params === 'prefers-reduced-motion: reduce' || + node.name === 'keyframe' || + node.name === 'charset'), + (node) => node.selector && node.selectors && !node.selectors.length, + (node) => node.selector && selectorTesters.some((fn) => fn(node.selector)), + (node) => + node.type === 'decl' && + (node.prop === 'transition' || + node.prop.indexOf('-webkit-') > -1 || + node.prop.indexOf('-ms-') > -1), + ]; +}); + +const getNodesToRemove = (nodes) => { + const removeTesters = getRemoveTesters(); + const remNodes = []; + + nodes.forEach((node) => { + if (removeTesters.some((fn) => fn(node))) { + remNodes.push(node); + } else if (node.nodes?.length) { + remNodes.push(...getNodesToRemove(node.nodes)); + } + }); + + return remNodes; +}; + +const getEmptyNodesToRemove = (nodes) => + nodes + .filter((node) => node.nodes) + .reduce((acc, node) => { + if (node.nodes.length) { + acc.push(...getEmptyNodesToRemove(node.nodes)); + } else { + acc.push(node); + } + + return acc; + }, []); + +const cleanCSS = (css) => { + const cssRoot = parse(css); + + getNodesToRemove(cssRoot.nodes).forEach((node) => { + node.remove(); + }); + + getEmptyNodesToRemove(cssRoot.nodes).forEach((node) => { + node.remove(); + }); + + return cssRoot.toResult().css; +}; + +module.exports = { cleanCSS }; diff --git a/scripts/frontend/startup_css/constants.js b/scripts/frontend/startup_css/constants.js new file mode 100644 index 00000000000..5f6189d9e59 --- /dev/null +++ b/scripts/frontend/startup_css/constants.js @@ -0,0 +1,106 @@ +const path = require('path'); +const IS_EE = require('../../../config/helpers/is_ee_env'); + +// controls -------------------------------------------------------------------- +const HTML_TO_REMOVE = [ + 'style', + 'script', + 'link[rel="stylesheet"]', + '.content-wrapper', + '#js-peek', + '.modal', + '.feature-highlight', + // The user has to open up the responsive nav, so we don't need it on load + '.top-nav-responsive', + // We don't want to capture all the children of a dropdown-menu + '.dropdown-menu', +]; +const CSS_TO_REMOVE = [ + '.tooltip', + '.tooltip.show', + '.fa', + '.gl-accessibility:focus', + '.toasted-container', + 'body .toasted-container.bottom-left', + '.popover', + '.with-performance-bar .navbar-gitlab', + '.text-secondary', + /\.feature-highlight-popover-content/, + /\.commit/, + /\.md/, + /\.with-performance-bar/, +]; +const APPLICATION_CSS_PREFIX = 'application'; +const APPLICATION_DARK_CSS_PREFIX = 'application_dark'; +const UTILITIES_CSS_PREFIX = 'application_utilities'; +const UTILITIES_DARK_CSS_PREFIX = 'application_utilities_dark'; + +// paths ----------------------------------------------------------------------- +const ROOT = path.resolve(__dirname, '../../..'); +const ROOT_RAILS = IS_EE ? path.join(ROOT, 'ee') : ROOT; +const FIXTURES_FOLDER_NAME = IS_EE ? 'fixtures-ee' : 'fixtures'; +const FIXTURES_ROOT = path.join(ROOT, 'tmp/tests/frontend', FIXTURES_FOLDER_NAME); +const PATH_SIGNIN_HTML = path.join(FIXTURES_ROOT, 'startup_css/sign-in.html'); +const PATH_ASSETS = path.join(ROOT, 'tmp/startup_css_assets'); +const PATH_STARTUP_SCSS = path.join(ROOT_RAILS, 'app/assets/stylesheets/startup'); + +// helpers --------------------------------------------------------------------- +const createMainOutput = ({ outFile, cssKeys, type }) => ({ + outFile, + htmlPaths: [ + path.join(FIXTURES_ROOT, `startup_css/project-${type}.html`), + path.join(FIXTURES_ROOT, `startup_css/project-${type}-legacy-menu.html`), + path.join(FIXTURES_ROOT, `startup_css/project-${type}-legacy-sidebar.html`), + path.join(FIXTURES_ROOT, `startup_css/project-${type}-signed-out.html`), + ], + cssKeys, + purgeOptions: { + safelist: { + standard: [ + 'page-with-icon-sidebar', + 'sidebar-collapsed-desktop', + // We want to include the root dropdown-menu style since it should be hidden by default + 'dropdown-menu', + ], + // We want to include the identicon backgrounds + greedy: [/^bg[0-9]$/], + }, + }, +}); + +const OUTPUTS = [ + createMainOutput({ + type: 'general', + outFile: 'startup-general', + cssKeys: [APPLICATION_CSS_PREFIX, UTILITIES_CSS_PREFIX], + }), + createMainOutput({ + type: 'dark', + outFile: 'startup-dark', + cssKeys: [APPLICATION_DARK_CSS_PREFIX, UTILITIES_DARK_CSS_PREFIX], + }), + { + outFile: 'startup-signin', + htmlPaths: [PATH_SIGNIN_HTML], + cssKeys: [APPLICATION_CSS_PREFIX, UTILITIES_CSS_PREFIX], + purgeOptions: { + safelist: { + standard: ['fieldset', 'hidden'], + deep: [/login-page$/], + }, + }, + }, +]; + +module.exports = { + HTML_TO_REMOVE, + CSS_TO_REMOVE, + APPLICATION_CSS_PREFIX, + APPLICATION_DARK_CSS_PREFIX, + UTILITIES_CSS_PREFIX, + UTILITIES_DARK_CSS_PREFIX, + ROOT, + PATH_ASSETS, + PATH_STARTUP_SCSS, + OUTPUTS, +}; diff --git a/scripts/frontend/startup_css/get_css_path.js b/scripts/frontend/startup_css/get_css_path.js new file mode 100644 index 00000000000..54078cf3149 --- /dev/null +++ b/scripts/frontend/startup_css/get_css_path.js @@ -0,0 +1,22 @@ +const fs = require('fs'); +const path = require('path'); +const { memoize } = require('lodash'); +const { PATH_ASSETS } = require('./constants'); +const { die } = require('./utils'); + +const listAssetsDir = memoize(() => fs.readdirSync(PATH_ASSETS)); + +const getCSSPath = (prefix) => { + const matcher = new RegExp(`^${prefix}-[^-]+\\.css$`); + const cssPath = listAssetsDir().find((x) => matcher.test(x)); + + if (!cssPath) { + die( + `Could not find the CSS asset matching "${prefix}". Have you run "scripts/frontend/startup_css/setup.sh"?`, + ); + } + + return path.join(PATH_ASSETS, cssPath); +}; + +module.exports = { getCSSPath }; diff --git a/scripts/frontend/startup_css/get_startup_css.js b/scripts/frontend/startup_css/get_startup_css.js new file mode 100644 index 00000000000..10e8371df8c --- /dev/null +++ b/scripts/frontend/startup_css/get_startup_css.js @@ -0,0 +1,69 @@ +const fs = require('fs'); +const cheerio = require('cheerio'); +const { mergeWith, isArray } = require('lodash'); +const { PurgeCSS } = require('purgecss'); +const purgeHtml = require('purgecss-from-html'); +const { cleanCSS } = require('./clean_css'); +const { HTML_TO_REMOVE } = require('./constants'); +const { die } = require('./utils'); + +const cleanHtml = (html) => { + const $ = cheerio.load(html); + + HTML_TO_REMOVE.forEach((selector) => { + $(selector).remove(); + }); + + return $.html(); +}; + +const mergePurgeCSSOptions = (...options) => + mergeWith(...options, (objValue, srcValue) => { + if (isArray(objValue)) { + return objValue.concat(srcValue); + } + + return undefined; + }); + +const getStartupCSS = async ({ htmlPaths, cssPaths, purgeOptions }) => { + const content = htmlPaths.map((htmlPath) => { + if (!fs.existsSync(htmlPath)) { + die(`Could not find fixture "${htmlPath}". Have you run the fixtures?`); + } + + const rawHtml = fs.readFileSync(htmlPath); + const html = cleanHtml(rawHtml); + + return { raw: html, extension: 'html' }; + }); + + const purgeCSSResult = await new PurgeCSS().purge({ + content, + css: cssPaths, + ...mergePurgeCSSOptions( + { + fontFace: true, + variables: true, + keyframes: true, + blocklist: [/:hover/, /:focus/, /-webkit-/, /-moz-focusring-/, /-ms-expand/], + safelist: { + standard: ['brand-header-logo'], + }, + // By default, PurgeCSS ignores special characters, but our utilities use "!" + defaultExtractor: (x) => x.match(/[\w-!]+/g), + extractors: [ + { + extractor: purgeHtml, + extensions: ['html'], + }, + ], + }, + purgeOptions, + ), + }); + + return purgeCSSResult.map(({ css }) => cleanCSS(css)).join('\n'); +}; + +module.exports = { getStartupCSS }; diff --git a/scripts/frontend/startup_css/main.js b/scripts/frontend/startup_css/main.js new file mode 100644 index 00000000000..1e8dcbebae2 --- /dev/null +++ b/scripts/frontend/startup_css/main.js @@ -0,0 +1,60 @@ +const { memoize } = require('lodash'); +const { OUTPUTS } = require('./constants'); +const { getCSSPath } = require('./get_css_path'); +const { getStartupCSS } = require('./get_startup_css'); +const { log, die } = require('./utils'); +const { writeStartupSCSS } = require('./write_startup_scss'); + +const memoizedCSSPath = memoize(getCSSPath); + +const runTask = async ({ outFile, htmlPaths, cssKeys, purgeOptions = {} }) => { + try { + log(`Generating startup CSS for HTML files: ${htmlPaths}`); + const generalCSS = await getStartupCSS({ + htmlPaths, + cssPaths: cssKeys.map(memoizedCSSPath), + purgeOptions, + }); + + log(`Writing to startup CSS...`); + const startupCSSPath = writeStartupSCSS(outFile, generalCSS); + log(`Finished writing to ${startupCSSPath}`); + + return { + success: true, + outFile, + }; + } catch (e) { + log(`ERROR! Unexpected error occurred while generating startup CSS for: ${outFile}`); + log(e); + + return { + success: false, + outFile, + }; + } +}; + +const main = async () => { + const result = await Promise.all(OUTPUTS.map(runTask)); + const fullSuccess = result.every((x) => x.success); + + log('RESULTS:'); + log('--------'); + + result.forEach(({ success, outFile }) => { + const status = success ? '✓' : 'ⅹ'; + + log(`${status}: ${outFile}`); + }); + + log('--------'); + + if (fullSuccess) { + log('Done!'); + } else { + die('Some tasks have failed'); + } +}; + +main(); diff --git a/scripts/frontend/startup_css/setup.sh b/scripts/frontend/startup_css/setup.sh new file mode 100755 index 00000000000..795799bd9fd --- /dev/null +++ b/scripts/frontend/startup_css/setup.sh @@ -0,0 +1,76 @@ +path_public_dir="public" +path_tmp="tmp" +path_dest="$path_tmp/startup_css_assets" +glob_css_dest="$path_dest/application*.css" +glob_css_src="$path_public_dir/assets/application*.css" +should_clean=false + +should_force() { + $1=="force" +} + +has_dest_already() { + find $glob_css_dest -quit +} + +has_src_already() { + find $glob_css_src -quit +} + +compile_assets() { + # We need to build the same test bundle that is built in CI + RAILS_ENV=test bundle exec rake rake:assets:precompile +} + +clean_assets() { + bundle exec rake rake:assets:clobber +} + +copy_assets() { + rm -rf $path_dest + mkdir $path_dest + cp $glob_css_src $path_dest +} + +echo "-----------------------------------------------------------" +echo "If you are run into any issues with Startup CSS generation," +echo "please check out the feedback issue:" +echo "" +echo "https://gitlab.com/gitlab-org/gitlab/-/issues/331812" +echo "-----------------------------------------------------------" + +if [ ! -e $path_public_dir ]; then + echo "Could not find '$path_public_dir/'. This script must be run in the root directory of the gitlab project." + exit 1 +fi + +if [ ! -e $path_tmp ]; then + echo "Could not find '$path_tmp/'. This script must be run in the root directory of the gitlab project." + exit 1 +fi + +if [ "$1" != "force" ] && has_dest_already; then + echo "Already found assets for '$glob_css_dest'. Did you want to run this script with 'force' argument?" + exit 0 +fi + +# If we are in CI, don't recompile things... +if [ -n "$CI" ]; then + if ! has_src_already; then + echo "Could not find '$glob_css_src'. Expected these artifacts to be generated by CI pipeline." + exit 1 + fi +elif has_src_already; then + echo "Found '$glob_css_src'. Skipping compile assets..." +else + echo "Starting compile assets process..." + compile_assets + should_clean=true +fi + +copy_assets + +if $should_clean; then + echo "Starting cleanup..." + clean_assets +fi diff --git a/scripts/frontend/startup_css/startup_css_changed.sh b/scripts/frontend/startup_css/startup_css_changed.sh new file mode 100755 index 00000000000..f214e61cdfb --- /dev/null +++ b/scripts/frontend/startup_css/startup_css_changed.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +echo "-----------------------------------------------------------" +echo "If you run into any issues with Startup CSS generation" +echo "please check out the feedback issue:" +echo "" +echo "https://gitlab.com/gitlab-org/gitlab/-/issues/331812" +echo "-----------------------------------------------------------" + +startup_glob="*stylesheets/startup*" + +echo "Staging changes to '${startup_glob}' so we can check for untracked files..." +git add ${startup_glob} + +if [ -n "$(git diff HEAD --name-only -- ${startup_glob})" ]; then + diff=$(git diff HEAD -- ${startup_glob}) + cat <<EOF + +Startup CSS changes detected! + +It looks like there have been recent changes which require +regenerating the Startup CSS files. + +**What should I do now?** + +IMPORTANT: Please make sure to update your MR title with "[RUN AS-IF-FOSS]" and start a new MR pipeline + +To fix this job, consider one of the following options: + + 1. Regenerating locally with "yarn run generate:startup_css". + 2. Copy and apply the following diff: + +----- start diff ----- +$diff + +----- end diff ------- +EOF + + exit 1 +fi diff --git a/scripts/frontend/startup_css/utils.js b/scripts/frontend/startup_css/utils.js new file mode 100644 index 00000000000..49ad201fb6b --- /dev/null +++ b/scripts/frontend/startup_css/utils.js @@ -0,0 +1,8 @@ +const die = (message) => { + console.log(message); + process.exit(1); +}; + +const log = (message) => console.error(`[gitlab.startup_css] ${message}`); + +module.exports = { die, log }; diff --git a/scripts/frontend/startup_css/write_startup_scss.js b/scripts/frontend/startup_css/write_startup_scss.js new file mode 100644 index 00000000000..245681bada3 --- /dev/null +++ b/scripts/frontend/startup_css/write_startup_scss.js @@ -0,0 +1,28 @@ +const { writeFileSync } = require('fs'); +const path = require('path'); +const prettier = require('prettier'); +const { PATH_STARTUP_SCSS } = require('./constants'); + +const buildFinalContent = (raw) => { + const content = `// DO NOT EDIT! This is auto-generated from "yarn run generate:startup_css" +// Please see the feedback issue for more details and help: +// https://gitlab.com/gitlab-org/gitlab/-/issues/331812 +@charset "UTF-8"; +${raw} +@import 'startup/cloaking'; +@include cloak-startup-scss(none); +`; + + // We run prettier so that there is more determinism with the generated file. + return prettier.format(content, { parser: 'scss' }); +}; + +const writeStartupSCSS = (name, raw) => { + const fullPath = path.join(PATH_STARTUP_SCSS, `${name}.scss`); + + writeFileSync(fullPath, buildFinalContent(raw)); + + return fullPath; +}; + +module.exports = { writeStartupSCSS }; |