mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-08-13 13:31:19 +00:00
307 lines
9.6 KiB
JavaScript
307 lines
9.6 KiB
JavaScript
import { mkdir, writeFile } from 'node:fs/promises';
|
|
import { fileURLToPath } from 'node:url';
|
|
import path from 'node:path';
|
|
import autoprefixer from 'autoprefixer';
|
|
import postcss from 'postcss';
|
|
import postcssCustomProperties from 'postcss-custom-properties';
|
|
import postcssGlobalData from '@csstools/postcss-global-data';
|
|
import { compile, Logger } from 'sass';
|
|
import glob from 'glob';
|
|
import tailwindcss from 'tailwindcss/lib/plugin.js';
|
|
import tailwindConfig from '../../../config/tailwind.config.js';
|
|
import IS_EE from '../../../config/helpers/is_ee_env.js';
|
|
import IS_JH from '../../../config/helpers/is_jh_env.js';
|
|
import { postCssColorToHex } from './postcss_color_to_hex.js';
|
|
|
|
const ROOT_PATH = path.resolve(import.meta.dirname, '../../../');
|
|
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.
|
|
*/
|
|
export 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
|
|
/*
|
|
This load path is added in order to be able to consume the bootstrap SCSS
|
|
from @gitlab/ui which has been vendored with:
|
|
https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/4333
|
|
*/
|
|
'node_modules/@gitlab/ui/src/vendor',
|
|
'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 };
|
|
});
|
|
}
|
|
|
|
function alwaysTrue() {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 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(filter) {
|
|
const inputGlobs = [
|
|
[
|
|
'app/assets/stylesheets/*.scss',
|
|
{
|
|
ignore: [
|
|
SCSS_PARTIAL_GLOB,
|
|
'**/bootstrap_migration*', // TODO: Prefix file name with _ (and/or move to framework)
|
|
],
|
|
},
|
|
],
|
|
[
|
|
'app/assets/stylesheets/{highlight/themes,lazy_bundles,lookbook,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);
|
|
const log = [];
|
|
for (const { source, dest } of sources) {
|
|
if (filter(source, dest)) {
|
|
log.push({ source, dest });
|
|
result.set(dest, source);
|
|
}
|
|
}
|
|
console.log(`${sourcePath} resolved to:`, log);
|
|
}
|
|
|
|
/*
|
|
* 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()));
|
|
}
|
|
|
|
export function resolveCompilationTargetsForVite() {
|
|
const targets = resolveCompilationTargets(() => true);
|
|
return Object.fromEntries(
|
|
Object.entries(targets).map(([source, dest]) => [dest.replace(OUTPUT_PATH, ''), source]),
|
|
);
|
|
}
|
|
|
|
function createPostCSSProcessors() {
|
|
return {
|
|
tailwind: postcss([tailwindcss(tailwindConfig), autoprefixer()]),
|
|
mailers: postcss([
|
|
tailwindcss(tailwindConfig),
|
|
postcssGlobalData({
|
|
files: [path.join(ROOT_PATH, 'node_modules/@gitlab/ui/src/tokens/build/css/tokens.css')],
|
|
}),
|
|
postcssCustomProperties({ preserve: false }),
|
|
postCssColorToHex(),
|
|
autoprefixer(),
|
|
]),
|
|
default: postcss([autoprefixer()]),
|
|
};
|
|
}
|
|
|
|
export async function compileAllStyles({
|
|
shouldWatch = false,
|
|
style = null,
|
|
filter = alwaysTrue,
|
|
} = {}) {
|
|
const reverseDependencies = {};
|
|
|
|
const compilationTargets = resolveCompilationTargets(filter);
|
|
|
|
const processors = createPostCSSProcessors();
|
|
|
|
const sassCompilerOptions = {
|
|
loadPaths: resolveLoadPaths(),
|
|
logger: Logger.silent,
|
|
// For now we compress CSS directly with SASS if we do not watch
|
|
// We probably want to change this later if there are more
|
|
// post-processing steps, because we would compress
|
|
// _after_ things like auto-prefixer, etc. happened
|
|
style: style ?? (shouldWatch ? 'expanded' : 'compressed'),
|
|
sourceMap: shouldWatch,
|
|
sourceMapIncludeSources: shouldWatch,
|
|
};
|
|
|
|
let fileWatcher = null;
|
|
if (shouldWatch) {
|
|
const { watch } = await import('chokidar');
|
|
fileWatcher = watch([]);
|
|
}
|
|
|
|
async function postProcessCSS(content, source) {
|
|
let processor = processors.default;
|
|
|
|
if (source.includes('/mailers/')) {
|
|
processor = processors.mailers;
|
|
} else if (content.css.includes('@apply')) {
|
|
processor = processors.tailwind;
|
|
}
|
|
|
|
return processor.process(content.css, {
|
|
from: source,
|
|
map: content.sourceMap
|
|
? {
|
|
prev: content.sourceMap,
|
|
inline: true,
|
|
sourcesContent: true,
|
|
}
|
|
: false,
|
|
});
|
|
}
|
|
|
|
async function compileSCSSFile(source, dest) {
|
|
console.log(`\tcompiling source ${source} to ${dest}`);
|
|
let content = compile(source, sassCompilerOptions);
|
|
if (fileWatcher) {
|
|
for (const dependency of content.loadedUrls) {
|
|
if (dependency.protocol === 'file:') {
|
|
const dependencyPath = fileURLToPath(dependency);
|
|
reverseDependencies[dependencyPath] ||= new Set();
|
|
reverseDependencies[dependencyPath].add(source);
|
|
fileWatcher.add(dependencyPath);
|
|
}
|
|
}
|
|
}
|
|
content = await postProcessCSS(content, source);
|
|
// Create target folder if it doesn't exist
|
|
await mkdir(path.dirname(dest), { recursive: true });
|
|
await writeFile(dest, content.css, 'utf-8');
|
|
}
|
|
|
|
if (fileWatcher) {
|
|
fileWatcher.on('change', async (changedFile) => {
|
|
console.warn(`${changedFile} changed, recompiling`);
|
|
const recompile = [];
|
|
for (const source of reverseDependencies[changedFile]) {
|
|
recompile.push(compileSCSSFile(source, compilationTargets[source]));
|
|
}
|
|
await Promise.all(recompile);
|
|
});
|
|
}
|
|
|
|
const initialCompile = Object.entries(compilationTargets).map(([source, dest]) =>
|
|
compileSCSSFile(source, dest),
|
|
);
|
|
|
|
await Promise.all(initialCompile);
|
|
|
|
return fileWatcher;
|
|
}
|
|
|
|
export function simplePluginForNodemon({ shouldWatch = true }) {
|
|
let fileWatcher = null;
|
|
return {
|
|
async start() {
|
|
await fileWatcher?.close();
|
|
fileWatcher = await compileAllStyles({ shouldWatch });
|
|
},
|
|
stop() {
|
|
return fileWatcher?.close();
|
|
},
|
|
};
|
|
}
|