UNPKG

css-mangle-webpack-plugin

Version:

This webpack plugin package is a CSS mangler that globally optimizes and shortens identifier names.

273 lines (226 loc) 10.7 kB
import { StringUtil } from "../utils/string"; import { Mangler } from "./mangler"; import { ManglerAsset, ManglerAssetType } from "./mangler_asset"; import { ManglerScript } from "./mangler_script"; import { ManglerContext } from "./mangler_context"; import { CSSQueryManglerContext, CSSVariableManglerOptions } from "./mangler_transpiler"; export abstract class ManglerReference<T = Mangler> { abstract transform(asset: ManglerAsset, context: ManglerContext<T>): string; } export class CSSVariableReference extends ManglerReference { constructor(public options: CSSVariableManglerOptions) { super(); } transform(asset: ManglerAsset, mangler: ManglerContext<Mangler>) { const t1 = this.options.literals ? this.transformLiteral(asset.syntaxText, mangler) : asset.syntaxText; const t2 = this.options.property ? this.transformProperty(t1, mangler) : t1; return t2; } transformLiteral(syntaxText: string, context: ManglerContext<Mangler>) { // References to CSS variables generally have a unique syntax, // but it can vary in different environments. // // Additionally, referencing variables is common not only in CSS-related files, // but also in script environments like `JS`, `JSX`, or bundled package scripts // where CSS variables might be declared or referenced. // // This patterns matched by the following are: // // - "--background" // - '--foreground' // const regexps = syntaxText.matchAll(/(?<="|')--[\w-]+(?=\\?\"|')/g); let replacedLength = 0; for (const regexp of regexps) { const index = regexp.index + replacedLength; const mangler = context.parent; const oldName = regexp[0]; const newName = mangler.CSSVariableOf(oldName, context.options.canUndeclared); if (newName) { const result = StringUtil.replaceRange( syntaxText, index, index + oldName.length, newName ); replacedLength += StringUtil.replacedLength(syntaxText, result); syntaxText = result; } } return syntaxText; } transformProperty(syntaxText: string, context: ManglerContext<Mangler>) { // This patterns matched by the following are: // // - var(--background, rgb(255, 255, 255)) // - var(--foreground) // const regexps = syntaxText.matchAll(/(?<=var\()[^()]*(?:\([^\)]*\)[^()]*)*(?=\))/g); let replacedLength = 0; for (const global of regexps) { const locals = global[0].matchAll(/--[\w-]+/g); // split const globalIndex = global.index; for (const local of locals) { const index = (globalIndex + local.index) + replacedLength; const mangler = context.parent; const oldName = local[0]; const newName = mangler.CSSVariableOf(oldName, context.options.canUndeclared); if (newName) { const result = StringUtil.replaceRange( syntaxText, index, index + oldName.length, newName ); replacedLength += StringUtil.replacedLength(syntaxText, result); syntaxText = result; } } } return syntaxText; } } export class CSSQueryReference extends ManglerReference<CSSQueryManglerContext> { transform( asset: ManglerAsset, context: ManglerContext<CSSQueryManglerContext> ): string { const t1 = this.transformHTML(asset.syntaxText, context); if (asset.syntaxType == ManglerAssetType.SCRIPT) { const t2 = this.transformScript(t1, context); const t3 = this.transformSingleQuery(t2, context); const t4 = this.transformMultplQuery(t3, context); return t4; } else { return t1; } } transformHTML( syntaxText: string, context: ManglerContext<CSSQueryManglerContext> ): string { const getPropertyRegexps = (name: string) => { return new RegExp(`(?<=<[\\w-]+ .*${name}\\s*=\\s*")[\\w\\s-]+?(?=".*>)`, "g"); } const cRegexps = syntaxText.matchAll(getPropertyRegexps("class")); const iRegexps = syntaxText.matchAll(getPropertyRegexps("id")); const manglers = context.parent; const createPropertyObject = (regexp: RegExpExecArray, prefix: string, mangler: Mangler) => { return {name: regexp[0], index: regexp.index, prefix, mangler}; } const cProperties = Array.from(cRegexps).map(r => createPropertyObject(r, ".", manglers.classMangler)); const iProperties = Array.from(iRegexps).map(r => createPropertyObject(r, "#", manglers.idMangler)); const properties = [...cProperties, ...iProperties]; for (const property of properties) { const propertyValues = property.name.matchAll(/[\w-]+/g); const propertyIndex = property.index; let replacedLength = 0; for (const propertyValue of propertyValues) { const oldName = propertyValue[0]; const newName = property.mangler.CSSPropertyOf(oldName, property.prefix); const length = oldName.length; const gIndex = propertyIndex + propertyValue.index + replacedLength; if (newName) { const result = StringUtil.replaceRange( syntaxText, gIndex, gIndex + length, newName.replace(property.prefix, "") ); replacedLength += StringUtil.replacedLength(syntaxText, result); syntaxText = result; } } } return syntaxText; } /** TODO: It should be considered about dereference for variables. */ transformScript( sources: string, context: ManglerContext<CSSQueryManglerContext> ): string { // for JSX const parser = new ManglerScript(sources); const getIdentifier = (prefix: string, mangler: Mangler, value: string) => { const identifiers = []; // Splitting given identifier values by "\s" value.split(" ").forEach(name => { identifiers.push(mangler.CSSPropertyOf(name, prefix, true) ?? name); }); return identifiers.filter(Boolean).join(" "); } const classNameBuilder = (oldName: string) => { return getIdentifier(".", context.parent.classMangler, oldName); } const idNameBuilder = (oldName: string) => { return getIdentifier("#", context.parent.idMangler, oldName); } parser.setPropertyByName("className", classNameBuilder); parser.setPropertyByName("id", idNameBuilder); return parser.code; } transformSingleQuery(syntaxText: string, context: ManglerContext<CSSQueryManglerContext>): string { const iRegexpInst = /(?<=\w.(getElementById)\(["'])\s*[\w-]+?\s*(?=["'])/g; const cRegexpInst = /(?<=\w.(getElementsByClassName)\(["'])\s*[\w-]+?\s*(?=["'])/g; const iRegexpList = Array.from(syntaxText.matchAll(iRegexpInst)); const cRegexpList = Array.from(syntaxText.matchAll(cRegexpInst)); const objectList = [ ...iRegexpList.map(r => {return {type: "id", instance: r}}), ...cRegexpList.map(r => {return {type: "class", instance: r}}), ]; let replacedLength = 0; for (const object of objectList) { const global = object.instance; const gIndex = global.index + replacedLength; const length = global[0].length; const oldName = global[0]; const newName = object.type == "id" ? context.parent.idMangler.CSSPropertyOf(oldName, "#") : context.parent.classMangler.CSSPropertyOf(oldName, "."); if (newName) { const result = StringUtil.replaceRange( syntaxText, gIndex, gIndex + length, newName.replace("#", "").replace(".", "") ); replacedLength += StringUtil.replacedLength(syntaxText, result); syntaxText = result; } } return syntaxText; } transformMultplQuery(syntaxText: string, context: ManglerContext<CSSQueryManglerContext>): string { const regexpInst = /(?<=\w.querySelector(All)?\(["'])[^]+?(?=["'])/g; const regexpList = syntaxText.matchAll(regexpInst); let replacedLength = 0; for (const global of regexpList) { const gIndex = global.index; const localId = Array.from(global[0].matchAll(/(?<=\#)[\w-]+/g)); // id const localCl = Array.from(global[0].matchAll(/(?<=\.)[\w-]+/g)); // class const objects = [ ...localId.map(r => {return {type: "id", instance: r}}), ...localCl.map(r => {return {type: "class", instance: r}}), ]; for (const object of objects) { const localI = object.instance; const lIndex = (gIndex + localI.index); const length = localI[0].length; const oldName = localI[0]; const newName = object.type == "id" ? context.parent.idMangler.CSSPropertyOf(oldName, "#") : context.parent.classMangler.CSSPropertyOf(oldName, "."); if (newName) { const result = StringUtil.replaceRange( syntaxText, lIndex, lIndex + length, newName.replace("#", "").replace(".", "") ); replacedLength += StringUtil.replacedLength(syntaxText, result); syntaxText = result; } } } return syntaxText; } }