import cssnano from "cssnano"; import postcss, { atRule } from "postcss"; import mqCombiner from "postcss-combine-media-query"; import sortMq from "postcss-sort-media-queries"; import fs from "fs/promises"; import path from "path"; import { fileURLToPath } from "url"; import { getFilesWithFS } from "#package/utils/node/getFilesWithFS.ts"; const __dirname = fileURLToPath(new URL(".", import.meta.url)); const DIST = path.resolve(__dirname, "../../../dist"); const STYLES_DIR = path.resolve(DIST, "styles"); const SCOPED_LAYER = "deUI"; const SCOPED_PREFIX = ":where(.deUI)"; const ROOT_CLASS = ".deUI"; const ROOT_STATE_CLASSES = [ ".isDomReady", ".isPageLoaded", ".isTouchscreen", ".isMobileDevice", ]; const getPrefixedSelector = (prefix: string, selector: string) => { if (selector.includes(ROOT_CLASS)) { const cleanedSelector = selector .replace(new RegExp(`\\s*${ROOT_CLASS}\\s*`, "g"), " ") .replace(/\s+/g, " ") .trim(); return cleanedSelector.length ? `${prefix} ${cleanedSelector}` : prefix; } return `${prefix} ${selector}`.replace(/\s+/g, " ").trim(); }; const normalizeRootRules = (selector: string, prefix: string) => { let normalizedSelector = selector; if (normalizedSelector.includes(`${prefix} :root`)) { normalizedSelector = normalizedSelector.replace(`${prefix} :root`, `:root${prefix}`); } ROOT_STATE_CLASSES.forEach((stateClass) => { if (normalizedSelector.startsWith(`${prefix} ${stateClass}`)) { normalizedSelector = normalizedSelector.replace(`${prefix} ${stateClass}`, `${prefix}${stateClass}`); } }); return normalizedSelector; }; const createScopePlugin = (prefix: string) => { return { postcssPlugin: "deui-scope-prefixer", Once(root: import("postcss").Root) { root.walkRules((rule) => { const parentName = rule.parent?.type === "atrule" ? rule.parent.name : ""; const keyframeRules = [ "keyframes", "-webkit-keyframes", "-moz-keyframes", "-o-keyframes", "-ms-keyframes", ]; if (keyframeRules.includes(parentName)) { return; } rule.selectors = rule.selectors.map((selector) => { return normalizeRootRules( getPrefixedSelector(prefix, selector), prefix ); }); }); }, }; }; const createLayerPlugin = (layerName: string) => { return { postcssPlugin: "deui-layer-wrapper", Once(root: import("postcss").Root) { const nodes = root.nodes ?? []; const charsets = nodes.filter((node) => { return node.type === "atrule" && node.name === "charset"; }); const contentNodes = nodes.filter((node) => { return !(node.type === "atrule" && node.name === "charset"); }); if (!contentNodes.length) { return; } if ( contentNodes.length === 1 && contentNodes[0].type === "atrule" && contentNodes[0].name === "layer" && contentNodes[0].params.trim() === layerName ) { return; } const layer = atRule({ name: "layer", params: layerName, }); contentNodes.forEach((node) => { node.remove(); layer.append(node); }); root.removeAll(); charsets.forEach((node) => root.append(node)); root.append(layer); }, }; }; const getScopedPath = (file: string) => { if (!file.startsWith(`${STYLES_DIR}${path.sep}`)) { return null; } let relativePath = path.relative(STYLES_DIR, file); if (relativePath.startsWith(`scoped${path.sep}`)) { return null; } if (relativePath.startsWith(`legacy${path.sep}`)) { relativePath = relativePath.slice(`legacy${path.sep}`.length); } return path.resolve(STYLES_DIR, "scoped", relativePath); }; await getFilesWithFS(DIST, ((file: string) => file.endsWith(".css")) ) .then(async (list) => { const processor = cssnano({ preset: [ "default", { discardComments: { removeAllButFirst: false, remove: (comment: string) => !comment.startsWith("!"), }, normalizeWhitespace: true, }, ], }); const combiner = mqCombiner(); const generatedStyles = await Promise.all( list.map(async (item: string) => { const scopedPath = getScopedPath(item); if (!scopedPath) { return []; } // eslint-disable-next-line security/detect-non-literal-fs-filename const css = await fs.readFile(item, "utf8"); // eslint-disable-next-line security/detect-non-literal-fs-filename await fs.mkdir(path.dirname(scopedPath), { recursive: true }); const scopedResult = await postcss([ createScopePlugin(SCOPED_PREFIX), createLayerPlugin(SCOPED_LAYER), ]) .process(css, { from: item, to: scopedPath, }); // eslint-disable-next-line security/detect-non-literal-fs-filename await fs.writeFile(scopedPath, scopedResult.css, "utf8"); return [ scopedPath ]; }) ); const filesToOptimize = Array.from(new Set([ ...list, ...generatedStyles.flat(), ])); const items = filesToOptimize.map((item: string) => new Promise(async (resolve: (value: void) => void, reject) => { // eslint-disable-next-line security/detect-non-literal-fs-filename const css = await fs.readFile(item, "utf8"); await postcss([ processor, combiner, sortMq({ sort: "mobile-first" }), ]) .process(css, { from: undefined, }) .then(async (result) => { if (result.css) { // eslint-disable-next-line security/detect-non-literal-fs-filename await fs.writeFile(item, result.css, "utf8"); return resolve(); } else { return reject(); } }); })); return Promise.all(items); }) .then(() => { console.debug("[CSSOptimisationPlugin] CSS optimized"); process.exit(0); }) .catch((err: Error) => { console.debug("[CSSOptimisationPlugin] Error", err); process.exit(1); });