Skip to content
Snippets Groups Projects
Verified Commit 98f6006a authored by Lukas Eipert's avatar Lukas Eipert Committed by GitLab
Browse files

Add script to compile SCSS with node sass package

The script `./scripts/frontend/compile_css.mjs` compiles our SCSS style
code to CSS. It follows the same rules/settings that our sassc-rails gem
currently does (compiling the same files) while using dart sass and not
the deprecated C implementation.

Compiled CSS files will be placed in `app/assets/builds` where they can
be picked up by the `cssbundling` gem. The gem _needs_ runs the npm
script `build:css`.

We also mark `sass` as a production dependency so that our license
scanning picks up everything properly.

One more note, the current MR doesn't integrate with the Vite and
Webpack dev servers yet, this would be done in a second step.

For more context see the in-progess MR:
gitlab-org/gitlab!140611



Co-Authored-By: default avatarMuhammed Ali <muhammed.ali@airtimerewards.com>
parent 4c7dbdc6
No related branches found
No related tags found
2 merge requests!144312Change service start (cut-off) date for code suggestions to March 15th,!141775Add script to compile SCSS with node sass package
......@@ -117,3 +117,6 @@ jest-snapshot-test-report.json
# https://vitejs.dev/guide/env-and-mode.html#env-files
*.local
/config/vite.gdk.json
# CSS compilation for cssbundling
app/assets/builds/
......@@ -90,7 +90,7 @@
.issuable-list li,
.issuable-info-container .controls {
.avatar-counter {
@include gl-pl-1
@include gl-pl-1;
@include gl-pr-2;
@include gl-h-5;
@include gl-min-w-5;
......
......@@ -10,6 +10,7 @@
"internal:eslint": "eslint --cache --max-warnings 0 --report-unused-disable-directives --ext .js,.vue,.graphql",
"internal:stylelint": "stylelint -q --rd '{ee/,}app/assets/stylesheets/**/*.{css,scss}'",
"prejest": "yarn check-dependencies",
"build:css": "node ./scripts/frontend/compile_css.mjs",
"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",
......@@ -189,6 +190,7 @@
"remark-gfm": "^3.0.1",
"remark-parse": "^10.0.2",
"remark-rehype": "^10.1.0",
"sass": "^1.69.7",
"scrollparent": "^2.0.1",
"semver": "^7.3.4",
"sentrybrowser": "npm:@sentry/browser@7.88.0",
......@@ -280,7 +282,6 @@
"nodemon": "^2.0.19",
"prettier": "2.2.1",
"prosemirror-test-builder": "^1.1.1",
"sass": "^1.69.0",
"stylelint": "^15.10.2",
"swagger-cli": "^4.0.4",
"timezone-mock": "^1.0.8",
......
#!/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();
......@@ -11789,10 +11789,10 @@ safe-regex@^2.1.1:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
sass@^1.69.0:
version "1.69.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.69.0.tgz#5195075371c239ed556280cf2f5944d234f42679"
integrity sha512-l3bbFpfTOGgQZCLU/gvm1lbsQ5mC/WnLz3djL2v4WCJBDrWm58PO+jgngcGRNnKUh6wSsdm50YaovTqskZ0xDQ==
sass@^1.69.7:
version "1.69.7"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.69.7.tgz#6e7e1c8f51e8162faec3e9619babc7da780af3b7"
integrity sha512-rzj2soDeZ8wtE2egyLXgOOHQvaC2iosZrkF6v3EUG+tBwEvhqUCzm0VP3k9gHF9LXbSrRhT5SksoI56Iw8NPnQ==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment