Skip to content

Commit 4134310

Browse files
committed
feat: init tailwindcss-mangle-shared
1 parent 6b0faa8 commit 4134310

File tree

12 files changed

+326
-0
lines changed

12 files changed

+326
-0
lines changed

packages/shared/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# tailwindcss-mangle-shared
2+
3+
The shared utils of tailwindcss-mangle
4+
5+
## Usage
6+
7+
// TODO

packages/shared/jest.config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { Config } from 'jest'
2+
import baseConfig from '../../jest.config'
3+
const config: Config = {
4+
projects: [
5+
{
6+
...baseConfig
7+
// transformIgnorePatterns: ['/node_modules/(?!(@parse5/)/tools)']
8+
}
9+
]
10+
}
11+
12+
export default config

packages/shared/package.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "tailwindcss-mangle-shared",
3+
"version": "0.0.1",
4+
"description": "The shared utils of tailwindcss-mangle",
5+
"main": "dist/index.js",
6+
"module": "./dist/index.mjs",
7+
"types": "dist/index.d.ts",
8+
"files": [
9+
"dist"
10+
],
11+
"scripts": {
12+
"dev": "cross-env NODE_ENV=development rollup -cw",
13+
"build": "cross-env NODE_ENV=production rollup -c",
14+
"dev:tsc": "tsc -p tsconfig.json --sourceMap",
15+
"build:tsc": "tsc -p tsconfig.json",
16+
"test": "yarn build && jest"
17+
},
18+
"keywords": [
19+
"tailwindcss",
20+
"patch",
21+
"core",
22+
"mangle"
23+
],
24+
"author": "SonOfMagic <[email protected]>",
25+
"license": "MIT",
26+
"publishConfig": {
27+
"access": "public",
28+
"registry": "https://registry.npmjs.org/"
29+
},
30+
"dependencies": {},
31+
"devDependencies": {},
32+
"homepage": "https://github.com/sonofmagic/tailwindcss-mangle",
33+
"repository": {
34+
"type": "git",
35+
"url": "git+https://github.com/sonofmagic/tailwindcss-mangle.git"
36+
}
37+
}

packages/shared/rollup.config.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { createRollupConfig } from '@icebreakers/rollup'
2+
// const isDev = process.env.NODE_ENV === 'development'
3+
export default createRollupConfig()

packages/shared/src/classGenerator.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import type { IClassGeneratorOptions, IClassGeneratorContextItem, IClassGenerator } from './types'
2+
3+
import { acceptChars, stripEscapeSequence, regExpTest } from './utils'
4+
5+
class ClassGenerator implements IClassGenerator {
6+
public newClassMap: Record<string, IClassGeneratorContextItem>
7+
public newClassSize: number
8+
public context: Record<string, any>
9+
public opts: IClassGeneratorOptions
10+
public classPrefix: string
11+
constructor(opts: IClassGeneratorOptions = {}) {
12+
this.newClassMap = {}
13+
this.newClassSize = 0
14+
this.context = {}
15+
this.opts = opts
16+
this.classPrefix = opts.classPrefix ?? 'tw-'
17+
}
18+
19+
defaultClassGenerate() {
20+
const chars = []
21+
let rest = (this.newClassSize - (this.newClassSize % acceptChars.length)) / acceptChars.length
22+
if (rest > 0) {
23+
while (true) {
24+
rest -= 1
25+
const m = rest % acceptChars.length
26+
const c = acceptChars[m]
27+
chars.push(c)
28+
rest -= m
29+
if (rest === 0) {
30+
break
31+
}
32+
rest /= acceptChars.length
33+
}
34+
}
35+
const prefixIndex = this.newClassSize % acceptChars.length
36+
37+
const newClassName = `${this.classPrefix}${acceptChars[prefixIndex]}${chars.join('')}`
38+
return newClassName
39+
}
40+
41+
ignoreClassName(className: string): boolean {
42+
return regExpTest(this.opts.ignoreClass, className)
43+
}
44+
45+
includeFilePath(filePath: string): boolean {
46+
const { include } = this.opts
47+
if (Array.isArray(include)) {
48+
return regExpTest(include, filePath)
49+
} else {
50+
return true
51+
}
52+
}
53+
54+
excludeFilePath(filePath: string): boolean {
55+
const { exclude } = this.opts
56+
if (Array.isArray(exclude)) {
57+
return regExpTest(exclude, filePath)
58+
} else {
59+
return false
60+
}
61+
}
62+
63+
isFileIncluded(filePath: string) {
64+
return this.includeFilePath(filePath) && !this.excludeFilePath(filePath)
65+
}
66+
67+
transformCssClass(className: string): string {
68+
const key = stripEscapeSequence(className)
69+
const cn = this.newClassMap[key]
70+
if (cn) return cn.name
71+
return className
72+
}
73+
74+
generateClassName(original: string): IClassGeneratorContextItem {
75+
const opts = this.opts
76+
77+
original = stripEscapeSequence(original)
78+
const cn = this.newClassMap[original]
79+
if (cn) return cn
80+
81+
let newClassName
82+
if (opts.customGenerate && typeof opts.customGenerate === 'function') {
83+
newClassName = opts.customGenerate(original, opts, this.context)
84+
}
85+
if (!newClassName) {
86+
newClassName = this.defaultClassGenerate()
87+
}
88+
89+
if (opts.reserveClassName && regExpTest(opts.reserveClassName, newClassName)) {
90+
if (opts.log) {
91+
console.log(`The class name has been reserved. ${newClassName}`)
92+
}
93+
this.newClassSize++
94+
return this.generateClassName(original)
95+
}
96+
if (opts.log) {
97+
console.log(`Minify class name from ${original} to ${newClassName}`)
98+
}
99+
const newClass: IClassGeneratorContextItem = {
100+
name: newClassName,
101+
usedBy: []
102+
}
103+
this.newClassMap[original] = newClass
104+
this.newClassSize++
105+
return newClass
106+
}
107+
}
108+
109+
export default ClassGenerator

packages/shared/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export { default as ClassGenerator } from './classGenerator'
2+
3+
export * from './regex'
4+
export * from './split'
5+
export * from './utils'
6+
export * from './types'

packages/shared/src/regex.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export function escapeStringRegexp(str: string) {
2+
if (typeof str !== 'string') {
3+
throw new TypeError('Expected a string')
4+
}
5+
return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d')
6+
}
7+
8+
export function makeRegex(str: string) {
9+
return new RegExp('(?<=^|[\\s"])' + escapeStringRegexp(str), 'g')
10+
}

packages/shared/src/split.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export const validateFilterRE = /[\w\u00A0-\uFFFF-_:%-?]/
2+
3+
export function isValidSelector(selector = ''): selector is string {
4+
return validateFilterRE.test(selector)
5+
}
6+
7+
export const splitCode = (
8+
code: string,
9+
options: {
10+
splitQuote?: boolean
11+
} = { splitQuote: true }
12+
) => {
13+
const regex = options.splitQuote ? /[\s"]+/ : /[\s]+/
14+
return code.split(regex).filter(isValidSelector)
15+
}

packages/shared/src/types.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export interface IClassGeneratorContextItem {
2+
name: string
3+
usedBy: any[]
4+
}
5+
6+
export interface IClassGeneratorOptions {
7+
reserveClassName?: (string | RegExp)[]
8+
customGenerate?: (original: string, opts: IClassGeneratorOptions, context: Record<string, any>) => string | undefined
9+
log?: boolean
10+
exclude?: (string | RegExp)[]
11+
include?: (string | RegExp)[]
12+
ignoreClass?: (string | RegExp)[]
13+
classPrefix?: string
14+
}
15+
16+
export interface IClassGenerator {
17+
newClassMap: Record<string, IClassGeneratorContextItem>
18+
newClassSize: number
19+
context: Record<string, any>
20+
}

packages/shared/src/utils.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import type { IClassGeneratorOptions, IClassGenerator } from './types'
2+
3+
export const defaultMangleClassFilter = (className: string) => {
4+
// ignore className like 'filter','container'
5+
// it may be dangerous to mangle/rename all StringLiteral , so use /-/ test for only those with /-/ like:
6+
// bg-[#123456] w-1 etc...
7+
return /[-:]/.test(className)
8+
}
9+
10+
export function groupBy<T>(arr: T[], cb: (arg: T) => string): Record<string, T[]> {
11+
if (!Array.isArray(arr)) {
12+
throw new Error('expected an array for first argument')
13+
}
14+
15+
if (typeof cb !== 'function') {
16+
throw new Error('expected a function for second argument')
17+
}
18+
19+
const result: Record<string, T[]> = {}
20+
for (let i = 0; i < arr.length; i++) {
21+
const item = arr[i]
22+
const bucketCategory = cb(item)
23+
const bucket = result[bucketCategory]
24+
25+
if (!Array.isArray(bucket)) {
26+
result[bucketCategory] = [item]
27+
} else {
28+
result[bucketCategory].push(item)
29+
}
30+
}
31+
32+
return result
33+
}
34+
35+
export const acceptChars = 'abcdefghijklmnopqrstuvwxyz'.split('')
36+
37+
export function stripEscapeSequence(words: string) {
38+
return words.replace(/\\/g, '')
39+
}
40+
41+
export const validate = (opts: IClassGeneratorOptions, classGenerator: IClassGenerator) => {
42+
if (!opts.log) return
43+
for (const className in classGenerator.newClassMap) {
44+
const c = classGenerator.newClassMap[className]
45+
if (c.usedBy.length >= 1) {
46+
continue
47+
}
48+
if (c.usedBy[0].match(/.+\.css:*$/)) {
49+
console.log(`The class name '${className}' is not used: defined at ${c.usedBy[0]}.`)
50+
} else {
51+
console.log(`The class name '${className}' is not defined: used at ${c.usedBy[0]}.`)
52+
}
53+
}
54+
}
55+
56+
export function isRegexp(value: unknown) {
57+
return Object.prototype.toString.call(value) === '[object RegExp]'
58+
}
59+
60+
export function isMap(value: unknown) {
61+
return Object.prototype.toString.call(value) === '[object Map]'
62+
}
63+
64+
export function regExpTest(arr: (string | RegExp)[] = [], str: string) {
65+
if (Array.isArray(arr)) {
66+
for (let i = 0; i < arr.length; i++) {
67+
const item = arr[i]
68+
if (typeof item === 'string') {
69+
if (item === str) {
70+
return true
71+
}
72+
} else if (isRegexp(item)) {
73+
item.lastIndex = 0
74+
if (item.test(str)) {
75+
return true
76+
}
77+
}
78+
}
79+
return false
80+
}
81+
throw new TypeError("paramater 'arr' should be a Array of Regexp | String !")
82+
}

0 commit comments

Comments
 (0)