Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2024-01-17 00:06:39 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2024-01-17 00:06:39 +0300
commitd62742b0169769191b32038cf20445a47db3b287 (patch)
tree0b718d822d0b3bffac1973dcff11b56853e664a2 /scripts
parente18006fc6313b1d04128416cdb5f1533adcdb53e (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/frontend/compile_css.mjs221
1 files changed, 221 insertions, 0 deletions
diff --git a/scripts/frontend/compile_css.mjs b/scripts/frontend/compile_css.mjs
new file mode 100755
index 00000000000..433c9117f81
--- /dev/null
+++ b/scripts/frontend/compile_css.mjs
@@ -0,0 +1,221 @@
+#!/usr/bin/env node
+
+import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import path from 'node:path';
+import { argv } from 'node:process';
+import { compile, Logger } from 'sass';
+import glob from 'glob';
+/* eslint-disable import/extensions */
+import IS_EE from '../../config/helpers/is_ee_env.js';
+import IS_JH from '../../config/helpers/is_jh_env.js';
+/* eslint-enable import/extensions */
+
+// Note, in node > 21.2 we could replace the below with import.meta.dirname
+const ROOT_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../');
+const OUTPUT_PATH = path.join(ROOT_PATH, 'app/assets/builds/');
+
+const BASE_PATH = 'app/assets/stylesheets';
+const EE_BASE_PATH = 'ee/app/assets/stylesheets';
+const JH_BASE_PATH = 'jh/app/assets/stylesheets';
+
+// SCSS files starting with an underscore are partials
+// and not meant to be compiled, usually
+const SCSS_PARTIAL_GLOB = '**/_*.scss';
+
+/**
+ * This function returns an array of paths where `sass` will look for includes
+ * It ensures that the `ee/` and `jh/` directories take precedence, so that the
+ * correct file is loaded.
+ */
+function resolveLoadPaths() {
+ const loadPaths = {
+ base: [BASE_PATH],
+ vendor: [
+ // no-op files
+ 'app/assets/stylesheets/_ee',
+ 'app/assets/stylesheets/_jh',
+ // loaded last
+ 'vendor/assets/stylesheets', // empty
+ 'node_modules',
+ ],
+ };
+
+ if (IS_EE) {
+ loadPaths.base.unshift(EE_BASE_PATH);
+ loadPaths.vendor.unshift('ee/app/assets/stylesheets/_ee');
+ }
+ if (IS_JH) {
+ loadPaths.base.unshift(JH_BASE_PATH);
+ loadPaths.vendor.unshift('jh/app/assets/stylesheets/_jh');
+ }
+ return Object.values(loadPaths)
+ .flat()
+ .map((p) => path.resolve(ROOT_PATH, p));
+}
+
+/**
+ *
+ * @param {string} globPath glob to be used for finding source files
+ * @param {Object} [options]
+ * @param {string[]} [options.ignore=['**\/_*.scss']] File names to be ignored (glob).
+ * Per default ignores SCSS partial files
+ * @param {string} [options.basePath='app/assets/javascripts'] Base path of the globPath.
+ * Will be used for the target to put the resulting files in the correct folder structure.
+ * @example
+ * // Assuming the folder contains bar.scss and the partial _baz.scss, this would return
+ * // [{
+ * // source: 'app/assets/stylesheets/foo/bar.scss',
+ * // dest: 'app/assets/builds/foo/bar.css'
+ * // }]
+ * findSourceFiles('app/assets/stylesheets/foo/*.scss')
+ * @returns {{source: string, dest: string }[]}
+ */
+function findSourceFiles(globPath, options = {}) {
+ const { ignore = [SCSS_PARTIAL_GLOB], basePath = BASE_PATH } = options;
+ console.log('Resolving source', globPath);
+
+ const scssPaths = path.join(ROOT_PATH, globPath);
+
+ return glob.sync(scssPaths, { ignore }).map((sourceFile) => {
+ const relSourcePath = path.relative(path.join(ROOT_PATH, basePath), sourceFile);
+ const destFile = path.join(OUTPUT_PATH, relSourcePath).replace(/\.scss$/, '.css');
+
+ return { source: sourceFile, dest: destFile };
+ });
+}
+
+/**
+ * This function returns a Map<inputPath, outputPath> of absolute paths
+ * which map from a SCSS source file to a CSS output file.
+ *
+ * The reason why it's a Map, rather than an array, if for example both
+ * - app/assets/stylesheets/page_bundles/milestone.scss
+ * - ee/app/assets/stylesheets/page_bundles/milestone.scss
+ * exist. Then only the latter needs to be compiled and the former ignored.
+ * In practise, the EE version often imports the CE version and extends it,
+ * but theoretically they could be completely separate files.
+ *
+ */
+function resolveCompilationTargets() {
+ const inputGlobs = [
+ [
+ 'app/assets/stylesheets/*.scss',
+ {
+ ignore: [
+ SCSS_PARTIAL_GLOB,
+ '**/bootstrap_migration*', // TODO: Prefix file name with _ (and/or move to framework)
+ '**/utilities.scss', // TODO: Prefix file name with _
+ ],
+ },
+ ],
+ [
+ 'app/assets/stylesheets/{highlight/themes,lazy_bundles,mailers,page_bundles,themes}/**/*.scss',
+ ],
+ // This is explicitly compiled to ensure that we do not end up with actual class definitions in this file
+ // See scripts/frontend/check_page_bundle_mixins_css_for_sideeffects.js
+ [
+ 'app/assets/stylesheets/page_bundles/_mixins_and_variables_and_functions.scss',
+ { ignore: [] },
+ ],
+ // TODO: Figure out why _these_ are compiled from within the highlight folder.
+ [
+ 'app/assets/stylesheets/highlight/{diff_custom_colors_addition.scss,diff_custom_colors_deletion.scss}',
+ ],
+ // TODO: find out why this is explicitly compiled
+ ['app/assets/stylesheets/themes/_dark.scss', { ignore: [] }],
+ ];
+
+ if (IS_EE) {
+ inputGlobs.push([
+ 'ee/app/assets/stylesheets/page_bundles/**/*.scss',
+ {
+ basePath: EE_BASE_PATH,
+ },
+ ]);
+ }
+
+ if (IS_JH) {
+ inputGlobs.push([
+ 'jh/app/assets/stylesheets/page_bundles/**/*.scss',
+ {
+ basePath: JH_BASE_PATH,
+ },
+ ]);
+ }
+
+ /**
+ * This is map mapping from outputPath => inputPath, to ensure that
+ * every outputPath just has a single source path.
+ * @type {Map<string, string>}
+ */
+ const result = new Map();
+
+ for (const [sourcePath, options] of inputGlobs) {
+ const sources = findSourceFiles(sourcePath, options);
+ console.log(`${sourcePath} resolved to:`, sources);
+ for (const { source, dest } of sources) {
+ result.set(dest, source);
+ }
+ }
+
+ /*
+ * Here we reverse the result map to be inputPath => outputPath,
+ * because for our further use cases we need the mapping this way.
+ */
+ return Object.fromEntries([...result.entries()].map((entry) => entry.reverse()));
+}
+
+function writeContentToFile(content, srcFile, outputFile) {
+ if (!existsSync(path.dirname(outputFile))) {
+ mkdirSync(path.dirname(outputFile), { recursive: true });
+ }
+ writeFileSync(outputFile, content.css);
+}
+
+async function compileAllStyles() {
+ const reverseDependencies = {};
+
+ const compilationTargets = resolveCompilationTargets();
+
+ const sassCompilerOptions = {
+ loadPaths: resolveLoadPaths(),
+ logger: Logger.silent,
+ };
+
+ let fileWatcher = null;
+ if (argv?.includes('--watch')) {
+ const { watch } = await import('chokidar');
+ fileWatcher = watch([]);
+ }
+
+ function compileSCSSFile(source, dest) {
+ console.log(`\tcompiling source ${source} to ${dest}`);
+ const content = compile(source, sassCompilerOptions);
+ if (fileWatcher) {
+ for (const dependency of content.loadedUrls) {
+ if (dependency.protocol === 'file:') {
+ reverseDependencies[dependency.pathname] ||= new Set();
+ reverseDependencies[dependency.pathname].add(source);
+ fileWatcher.add(dependency.pathname);
+ }
+ }
+ }
+ writeContentToFile(content, source, dest);
+ }
+
+ if (fileWatcher) {
+ fileWatcher.on('change', (changedFile) => {
+ console.warn(`${changedFile} changed, recompiling`);
+ for (const source of reverseDependencies[changedFile]) {
+ compileSCSSFile(source, compilationTargets[source]);
+ }
+ });
+ }
+
+ for (const [source, dest] of Object.entries(compilationTargets)) {
+ compileSCSSFile(source, dest);
+ }
+}
+
+await compileAllStyles();