All files / packages/design-system/scripts/gulp/generate sanitized-components.js

0% Statements 0/77
0% Branches 0/14
0% Functions 0/11
0% Lines 0/67

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140                                                                                                                                                                                                                                                                                       
// Copyright (c) 2015-present, salesforce.com, inc. All rights reserved
// Licensed under BSD 3-Clause - see LICENSE.txt or git.io/sfdc-license
 
/*
 * Compiles the sanitized per-component stylesheets (`.css/**` -> `dist/css/**`)
 * across a pool of worker threads.
 *
 * Each file is independent and the work (Sass + PostCSS + two Prettier passes)
 * is CPU-bound, so a gulp stream compiles them one at a time on a single core.
 * The per-file steps mirror the gulp pipeline this replaced exactly:
 * gulp-sass -> gulp-postcss -> gulp-prettier -> strip BEM modifiers ->
 * gulp-prettier. Sass, PostCSS and Prettier are resolved the same way the gulp
 * plugins resolve them so the output stays byte-identical.
 *
 * This file is both the pool (main thread) and the worker entry, and is plain
 * CommonJS so workers can load it without @babel/register.
 */
 
const fs = require('fs');
const os = require('os');
const path = require('path');
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
 
/**
 * Regex to remove BEM -- modifier from source
 */
const BEM_MODIFIER_REGEX = [/(\.\S.*[\S]+.(--)+.*)(\,)+?\B/g, /\,[\n\s]*(\.\S.*[\S]+.(--)+.*)\b(\s|\]\))/g];
 
// Load a dependency from the gulp plugin that used to own this step, so the
// worker runs the same copy (gulp-prettier, for instance, nests its own prettier).
const requireFrom = (plugin, id) => require(require.resolve(id, { paths: [path.dirname(require.resolve(plugin))] }));
 
const compileFile = async ({ srcPath, srcBase, destBase }) => {
  const sass = require('sass');
  const postcss = requireFrom('gulp-postcss', 'postcss');
  const prettier = requireFrom('gulp-prettier', 'prettier');
  const discardComments = require('postcss-discard-comments');
  const { sassOptions, postCssPlugins, PRETTIER_CSS_OPTIONS } = require('../../helpers/css-options');
 
  const source = fs.readFileSync(srcPath, 'utf8');
  // gulp-sass renames the file before the rest of the pipeline sees it
  const cssPath = srcPath.replace(/\.scss$/, '.css');
 
  let css = '';
  if (source.length) {
    const options = sassOptions({ outputStyle: 'expanded' });
    // gulp-sass prepends the file's own directory to the load paths
    options.loadPaths = [path.dirname(srcPath), ...options.loadPaths];
    css = sass.compileString(source, options).css;
  }
 
  css = (await postcss(postCssPlugins([discardComments()])).process(css, { from: cssPath, to: cssPath, map: false }))
    .css;
 
  const format = async (code) => {
    const config = await prettier.resolveConfig(cssPath, PRETTIER_CSS_OPTIONS);
    return prettier.format(code, { ...config, ...PRETTIER_CSS_OPTIONS, filepath: cssPath });
  };
 
  // Need to format to ensure proper parsing
  css = await format(css);
  css = css.replace(BEM_MODIFIER_REGEX[0], '').replace(BEM_MODIFIER_REGEX[1], '');
  // For cleanup
  css = await format(css);
 
  const destPath = path.join(destBase, path.relative(srcBase, cssPath));
  fs.mkdirSync(path.dirname(destPath), { recursive: true });
  fs.writeFileSync(destPath, css);
};
 
// Workers claim files by bumping a shared counter rather than asking the main
// thread for more work: during `dist` the main thread is busy compiling the
// full sanitized stylesheet, and round-tripping through it stalls the pool.
const runWorker = async ({ srcPaths, srcBase, destBase, counter }) => {
  const next = new Int32Array(counter);
  for (let i = Atomics.add(next, 0, 1); i < srcPaths.length; i = Atomics.add(next, 0, 1)) {
    try {
      await compileFile({ srcPath: srcPaths[i], srcBase, destBase });
    } catch (error) {
      // Errors survive structured clone with their message and stack intact
      parentPort.postMessage({ ok: false, srcPath: srcPaths[i], error });
      return;
    }
  }
  parentPort.postMessage({ ok: true });
};
 
if (!isMainThread) runWorker(workerData);
 
// Past ~8 workers, startup cost and contention outweigh the extra parallelism
const MAX_WORKERS = 8;
 
/**
 * Compiles every `srcPaths` entry into `destBase`, preserving its path
 * relative to `srcBase`. Rejects on the first failure.
 * @param {{ srcPaths: string[], srcBase: string, destBase: string }} options
 * @returns {Promise<void>}
 */
const compileSanitizedComponents = ({ srcPaths, srcBase, destBase }) =>
  new Promise((resolve, reject) => {
    const size = Math.min(srcPaths.length, MAX_WORKERS, Math.max(1, os.availableParallelism() - 1));
    const counter = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
    const workers = [];
    let running = size;
    let failed = false;
 
    if (!size) return resolve();
 
    const fail = (error) => {
      if (failed) return;
      failed = true;
      workers.forEach((worker) => worker.terminate());
      reject(error);
    };
 
    // Name the failing file but keep the worker's error as the cause. The stack
    // is the worker's too: gulp prints only `stack`, and this side's is noise.
    const fileError = ({ srcPath, error }) => {
      const file = path.relative(srcBase, srcPath);
      const wrapped = new Error(`${file}\n${error.message}`, { cause: error });
      wrapped.stack = `${file}\n${error.stack}`;
      return wrapped;
    };
 
    for (let i = 0; i < size; i++) {
      const worker = new Worker(__filename, { workerData: { srcPaths, srcBase, destBase, counter } });
      workers.push(worker);
      worker.on('message', (result) => {
        if (!result.ok) return fail(fileError(result));
        if (--running === 0) resolve();
      });
      worker.on('error', fail);
      worker.on('exit', (code) => {
        if (code !== 0) fail(new Error(`sanitized component worker exited with code ${code}`));
      });
    }
  });
 
module.exports = { BEM_MODIFIER_REGEX, compileSanitizedComponents };