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
text/typescript
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;
}
}