Skip to content

Commit 838f88c

Browse files
Merge pull request #35 from linked-planet/dev
Next Release
2 parents e5c1483 + d2cbd3f commit 838f88c

File tree

116 files changed

+13319
-10345
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

116 files changed

+13319
-10345
lines changed

biome.json

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
{
22
"$schema": "https://biomejs.dev/schemas/1.7.0/schema.json",
3-
"formatter": {
4-
"enabled": false,
5-
"formatWithErrors": false,
3+
"formatter": {
4+
"enabled": true,
5+
"formatWithErrors": true,
66
"indentStyle": "tab",
77
"indentWidth": 4,
88
"lineEnding": "lf",
99
"lineWidth": 80,
1010
"attributePosition": "auto"
1111
},
1212
"organizeImports": { "enabled": true },
13+
"css": {
14+
"formatter": {
15+
"enabled": true
16+
}
17+
},
1318
"linter": {
1419
"enabled": true,
15-
"rules": {
16-
"recommended": true,
20+
"rules": {
21+
"recommended": true,
1722
"complexity": {
1823
"noForEach": "off"
24+
},
25+
"correctness": {
26+
"noUnusedImports": "warn"
1927
}
2028
},
2129
"ignore": [
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type postcss from "postcss"
2+
3+
// this is a postcss plugin that prefixes the classes in the css files
4+
export function postcssClassPrefixerPlugin({
5+
prefix,
6+
classes,
7+
}: {
8+
prefix: string
9+
classes: string[]
10+
}) {
11+
const classesToPrefixRegex = new RegExp(`(${classes.join("|")})`, "g")
12+
const ret: postcss.Plugin = {
13+
postcssPlugin: "css-class-prefixer",
14+
Rule: (rule) => {
15+
rule.selectors = rule.selectors.map((selector) => {
16+
return selector.replace(classesToPrefixRegex, (match) => {
17+
return `${prefix}${match}`
18+
})
19+
})
20+
},
21+
}
22+
return ret
23+
}
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
import generate from "@babel/generator"
2+
import { parse } from "@babel/parser"
3+
import { default as traverse } from "@babel/traverse"
4+
5+
import { existsSync } from "node:fs"
6+
import fs from "node:fs/promises"
7+
import * as t from "@babel/types"
8+
import postcss from "postcss"
9+
import type { OutputAsset, Plugin } from "rollup"
10+
11+
let classPrefix = ""
12+
let classesToBePrefix: string[] = []
13+
let jsFilePostfixes: string[] = []
14+
15+
//JS:
16+
let classesToPrefixRegexJS: RegExp
17+
18+
//CSS:
19+
let classesToPrefixRegexCSS: RegExp
20+
21+
const bgenerate = generate.default
22+
const btraverse = traverse.default
23+
24+
// the css prefixing is done by postcss
25+
async function prefixCSS(code: string, fileName: string) {
26+
const pcssPrefixerPlugin: postcss.Plugin = {
27+
postcssPlugin: "css-class-prefixer",
28+
Rule: (rule) => {
29+
rule.selectors = rule.selectors.map((selector) => {
30+
return selector.replace(classesToPrefixRegexCSS, (match) => {
31+
return `.${classPrefix}${match.substring(1)}`
32+
})
33+
})
34+
},
35+
}
36+
37+
const processor = postcss([pcssPrefixerPlugin])
38+
const result = (await processor.process(code)).content
39+
return result
40+
}
41+
42+
// Helper function to handle both JSX and plain class attributes
43+
function handleClassNameValue(classNameValueNode, prefixFunc) {
44+
if (t.isStringLiteral(classNameValueNode)) {
45+
const stringVal = classNameValueNode.value
46+
const prefixedValue = prefixFunc(stringVal)
47+
classNameValueNode.value = prefixedValue
48+
} else if (t.isJSXExpressionContainer(classNameValueNode)) {
49+
const expressionContainer = classNameValueNode.expression
50+
if (t.isStringLiteral(expressionContainer)) {
51+
const stringVal = expressionContainer.value
52+
const prefixedValue = prefixFunc(stringVal)
53+
expressionContainer.value = prefixedValue
54+
} else if (t.isTemplateLiteral(expressionContainer)) {
55+
for (const expression of expressionContainer.expressions) {
56+
if (t.isStringLiteral(expression)) {
57+
const stringVal = expression.value
58+
const prefixedValue = prefixFunc(stringVal)
59+
expression.value = prefixedValue
60+
}
61+
}
62+
for (const element of expressionContainer.quasis) {
63+
const stringVal = element.value.raw
64+
const prefixedValue = prefixFunc(stringVal)
65+
element.value.raw = prefixedValue
66+
67+
const cstringVal = element.value.cooked
68+
if (cstringVal) {
69+
const cprefixedValue = prefixFunc(cstringVal, false)
70+
element.value.cooked = cprefixedValue
71+
}
72+
}
73+
}
74+
}
75+
}
76+
77+
// Does the actual prefixing
78+
let replacements = 0
79+
const prefixFunc = (stringVal: string, shouldCount = true) => {
80+
classesToPrefixRegexJS.lastIndex = 0
81+
let _replacements = 0
82+
const replaced = stringVal.replace(classesToPrefixRegexJS, (match) => {
83+
_replacements++
84+
return `${classPrefix}${match}`
85+
})
86+
if (_replacements) {
87+
if (shouldCount) {
88+
replacements += _replacements
89+
}
90+
return replaced
91+
}
92+
return stringVal
93+
}
94+
95+
// Helper function to handle TemplateLiteral nodes
96+
function handleTemplateLiteral(templateLiteralNode) {
97+
for (const expression of templateLiteralNode.expressions) {
98+
if (t.isStringLiteral(expression)) {
99+
const stringVal = expression.value
100+
const prefixedValue = prefixFunc(stringVal)
101+
expression.value = prefixedValue
102+
}
103+
}
104+
for (const element of templateLiteralNode.quasis) {
105+
const stringVal = element.value.raw
106+
const prefixedValue = prefixFunc(stringVal)
107+
element.value.raw = prefixedValue
108+
109+
const cstringVal = element.value.cooked
110+
if (cstringVal) {
111+
const cprefixedValue = prefixFunc(cstringVal, false)
112+
element.value.cooked = cprefixedValue
113+
}
114+
}
115+
}
116+
117+
/**
118+
* Uses a regex to prefix the classes inside className or class strings
119+
*/
120+
async function prefixJSX(code: string, fileName: string) {
121+
const ast = parse(code, {
122+
sourceType: "module",
123+
plugins: ["jsx", "typescript"],
124+
})
125+
126+
btraverse(ast, {
127+
JSXAttribute(path) {
128+
if (t.isJSXIdentifier(path.node.name, { name: "className" })) {
129+
const classNameValueNode: t.Node = path.node.value
130+
handleClassNameValue(classNameValueNode, prefixFunc)
131+
}
132+
},
133+
ObjectProperty(path) {
134+
if (
135+
t.isIdentifier(path.node.key, { name: "className" }) ||
136+
t.isIdentifier(path.node.key, { name: "class" })
137+
) {
138+
if (t.isStringLiteral(path.node.value)) {
139+
const prefixedValue = prefixFunc(path.node.value.value)
140+
path.node.value = t.stringLiteral(prefixedValue)
141+
}
142+
}
143+
},
144+
145+
// Handle TypeScript properties like `this.className = 'some-class'`
146+
ClassProperty(path) {
147+
if (t.isIdentifier(path.node.key, { name: "className" })) {
148+
if (t.isStringLiteral(path.node.value)) {
149+
const prefixedValue = prefixFunc(path.node.value.value)
150+
path.node.value = t.stringLiteral(prefixedValue)
151+
}
152+
}
153+
if (t.isIdentifier(path.node.key, { name: "class" })) {
154+
if (t.isStringLiteral(path.node.value)) {
155+
const prefixedValue = prefixFunc(path.node.value.value)
156+
path.node.value = t.stringLiteral(prefixedValue)
157+
}
158+
}
159+
},
160+
161+
// Handle JSX attributes in the transpiled React.createElement calls
162+
CallExpression(path) {
163+
const callee = path.get("callee")
164+
// Check if it's a call to jsxRuntimeExports.jsx or jsxRuntimeExports.jsxs
165+
if (
166+
callee.isMemberExpression() &&
167+
t.isIdentifier(callee.node.object, {
168+
name: "jsxRuntimeExports",
169+
}) &&
170+
(t.isIdentifier(callee.node.property, { name: "jsx" }) ||
171+
t.isIdentifier(callee.node.property, { name: "jsxs" }))
172+
) {
173+
const args = path.get("arguments")
174+
if (args.length > 1) {
175+
const props = args[1]
176+
if (props.isObjectExpression()) {
177+
// biome-ignore lint/complexity/noForEach: <explanation>
178+
props.get("properties").forEach((propPath) => {
179+
if (t.isObjectProperty(propPath.node)) {
180+
const key = propPath.get("key")
181+
if (key.isIdentifier({ name: "className" })) {
182+
const value = propPath.get("value")
183+
if (value.isStringLiteral()) {
184+
const stringVal = value.node.value
185+
const prefixedValue =
186+
prefixFunc(stringVal)
187+
value.replaceWith(
188+
t.stringLiteral(prefixedValue),
189+
)
190+
} else if (value.isTemplateLiteral()) {
191+
handleTemplateLiteral(value.node)
192+
}
193+
}
194+
}
195+
})
196+
}
197+
}
198+
}
199+
200+
// handle React.createElement calls - untested
201+
if (
202+
callee.isIdentifier({ name: "React" }) ||
203+
callee.isIdentifier({ name: "createElement" })
204+
) {
205+
const args = path.get("arguments")
206+
if (args.length > 1) {
207+
const props = args[1] // props is the second argument
208+
if (props.isObjectExpression()) {
209+
// biome-ignore lint/complexity/noForEach: <explanation>
210+
props.get("properties").forEach((propPath) => {
211+
if (t.isObjectProperty(propPath.node)) {
212+
const key = propPath.get("key")
213+
if (
214+
key.isIdentifier({ name: "className" }) ||
215+
key.isIdentifier({ name: "class" })
216+
) {
217+
const value = propPath.get("value")
218+
if (value.isStringLiteral()) {
219+
const stringVal = value.node.value
220+
const prefixedValue =
221+
prefixFunc(stringVal)
222+
value.replaceWith(
223+
t.stringLiteral(prefixedValue),
224+
)
225+
} else if (value.isTemplateLiteral()) {
226+
handleTemplateLiteral(value.node)
227+
}
228+
}
229+
}
230+
})
231+
}
232+
}
233+
}
234+
},
235+
})
236+
237+
const updatedCode = bgenerate(ast).code as string
238+
if (replacements) {
239+
console.log(
240+
"\x1b[33m%s\x1b[0m",
241+
`\n[rollup-plugin-class-prefixer] - Prefixed ${replacements} classes: ${fileName}`,
242+
)
243+
}
244+
245+
return { code: updatedCode, replacements }
246+
}
247+
248+
/**
249+
* Rollup plugin that prefixes the classes in the JS(X) and TS(X) files.
250+
*/
251+
export default function classPrefixerPlugin({
252+
prefix,
253+
classes,
254+
jsFiles = ["js", "jsx", "ts", "tsx"],
255+
cssFiles = ["css", "scss"],
256+
}: {
257+
prefix: string
258+
classes: string[]
259+
jsFiles?: string[]
260+
cssFiles?: string[]
261+
}) {
262+
classPrefix = prefix
263+
classesToBePrefix = classes
264+
jsFilePostfixes = jsFiles
265+
classesToPrefixRegexJS = new RegExp(
266+
`\\b(${classesToBePrefix.join("|")})\\b`,
267+
"g",
268+
)
269+
classesToPrefixRegexCSS = new RegExp(
270+
`\\.(${classesToBePrefix.join("|")})(?![a-zA-Z0-9_-])`,
271+
"g",
272+
)
273+
const ret: Plugin = {
274+
name: "class-prefixer-plugin",
275+
// prefixes .js, .ts, .tsx, .jsx files in the load step, but tailwindcss has not build yet the classes at this point
276+
async load(id) {
277+
if (!existsSync(id)) {
278+
return null
279+
}
280+
281+
replacements = 0
282+
283+
const ext = id.split(".").pop()
284+
if (ext && jsFilePostfixes.includes(ext)) {
285+
const data = await fs.readFile(id, "utf-8")
286+
const { code } = await prefixJSX(data, id)
287+
return code
288+
}
289+
290+
return null
291+
},
292+
293+
// this prefixes the css files in the generateBundle step
294+
async generateBundle(options, bundle) {
295+
for (const fileName of Object.keys(bundle)) {
296+
const fileEnd = fileName.split(".").pop()
297+
if (fileEnd && cssFiles.includes(fileEnd)) {
298+
const asset = bundle[fileName] as OutputAsset
299+
const code = asset.source as string
300+
const prefixed = await prefixCSS(code, fileName)
301+
asset.source = prefixed
302+
}
303+
}
304+
},
305+
}
306+
return ret
307+
}

0 commit comments

Comments
 (0)