Skip to content

Commit

Permalink
Perf(ESLint): Use workers to share configuration and speed up expensi…
Browse files Browse the repository at this point in the history
…ve style calculations
  • Loading branch information
1aron committed Nov 13, 2023
1 parent e5c9ccc commit e66a680
Show file tree
Hide file tree
Showing 29 changed files with 318 additions and 168 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,6 @@ packages/extractor/**/*master.css.*
packages/extractor.vite/tests/vite/index.html
packages/extractor.vite/tests/vite/master.css.*
packages/extractor.webpack/tests/webpack/src/index.html
packages/extractor.webpack/tests/webpack/src/master.css.*
packages/extractor.webpack/tests/webpack/src/master.css.*

**/*/cache
1 change: 1 addition & 0 deletions examples/eslint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"devDependencies": {
"@angular-eslint/template-parser": "^16.2.0",
"@master/css.react": "workspace:^",
"@master/css": "workspace:^",
"@master/eslint-config-css": "workspace:^",
"@master/eslint-plugin-css": "workspace:^",
"@typescript-eslint/parser": "^6.9.0",
Expand Down
11 changes: 8 additions & 3 deletions packages/css/src/functions/reorder-for-readable-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ import { MasterCSS } from '../core'
/**
* Sorts classes in a consistent order
* @param classes
* @param config
* @param options
* @returns consistent classes
*/
export default function reorderForReadableClasses(classes: string[], config?: Config) {
export default function reorderForReadableClasses(classes: string[], options?: { css?: MasterCSS, config?: Config }) {
if (!classes.length) return
const css = new MasterCSS(config)
let css: MasterCSS
if (options?.css) {
css = options?.css
} else {
css = new MasterCSS(options?.config)
}
for (const eachClass of classes) {
css.add(eachClass)
}
Expand Down
9 changes: 6 additions & 3 deletions packages/eslint-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,20 @@
"dependencies": {
"@master/css": "workspace:^",
"@master/css-validator": "workspace:^",
"explore-config": "^2.5.19"
"explore-config": "^2.5.19",
"synckit": "^0.8.5"
},
"devDependencies": {
"@angular-eslint/template-parser": "^16.2.0",
"@swc-node/register": "^1.6.8",
"@types/node": "^20.6.0",
"@typescript-eslint/parser": "^5.50.0",
"@typescript-eslint/utils": "^6.9.0",
"@typescript-eslint/parser": "^6.10.0",
"@typescript-eslint/utils": "^6.10.0",
"eslint": "^8.52.0",
"eslint-plugin-markdown": "^3.0.1",
"eslint-plugin-mdx": "^2.2.0",
"svelte-eslint-parser": "^0.33.1",
"ts-node": "^10.9.1",
"vue-eslint-parser": "^9.3.1"
}
}
19 changes: 7 additions & 12 deletions packages/eslint-plugin/src/rules/class-collision.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as astUtil from '../utils/ast'
import areDeclarationsEqual from '../utils/are-declarations-equal'
import defineVisitors from '../utils/define-visitors'
import resolveContext from '../utils/resolve-context'
import { createValidRules } from '@master/css-validator'
import { Rule } from 'eslint'
import findLoc from '../utils/find-loc'
import { parseNodeRecursive } from '../utils/parse-node-recursive'
import validRulesAction from '../utils/valid-rules-action'

export default {
meta: {
Expand Down Expand Up @@ -40,27 +41,21 @@ export default {
],
},
create(context) {
const { options, settings, config } = resolveContext(context)
const { options, settings } = resolveContext(context)
const visitNode = (node, arg = null) => {
astUtil.parseNodeRecursive(
parseNodeRecursive(
node,
arg,
(classNames, node, originalClassNamesValue, start, end) => {
const sourceCode = context.sourceCode
const sourceCodeLines = sourceCode.lines
const nodeStartLine = node.loc.start.line
const nodeEndLine = node.loc.end.line
const ruleOfClass = {}
classNames
.forEach(eachClassName => {
ruleOfClass[eachClassName] = createValidRules(eachClassName, { config })[0]
})

const ruleOfClass = validRulesAction(classNames, settings.config)
for (let i = 0; i < classNames.length; i++) {
const className = classNames[i]
const rule = ruleOfClass[className]
const conflicts = []

if (rule) {
for (let j = 0; j < classNames.length; j++) {
const compareClassName = classNames[j]
Expand All @@ -83,7 +78,7 @@ export default {
fixClassNames = fixClassNames.replace(new RegExp(`\\s+${regexSafe}|${regexSafe}\\s+`), '')
}
context.report({
loc: astUtil.findLoc(className, sourceCodeLines, nodeStartLine, nodeEndLine),
loc: findLoc(className, sourceCodeLines, nodeStartLine, nodeEndLine),
messageId: 'collisionClass',
data: {
message: `"${className}" applies the same CSS declarations as ${conflictClassNamesMsg}.`,
Expand Down
29 changes: 17 additions & 12 deletions packages/eslint-plugin/src/rules/class-order.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
/* eslint-disable no-case-declarations */
import * as astUtil from '../utils/ast'
import defineVisitors from '../utils/define-visitors'
import resolveContext from '../utils/resolve-context'
import { reorderForReadableClasses } from '@master/css'
import { Rule } from 'eslint'
import getTemplateElementBody from '../utils/get-template-element-body'
import getTemplateElementSuffix from '../utils/get-template-element-suffix'
import getTemplateElementPrefix from '../utils/get-template-element-prefix'
import extractValueFromNode from '../utils/extract-value-from-node'
import extractRangeFromNode from '../utils/extract-range-from-node'
import extractClassnamesFromValue from '../utils/extract-classnames-from-value'
import findLoc from '../utils/find-loc'
import reorderValidClassesAction from '../utils/reorder-valid-classes-action'

export default {
meta: {
Expand Down Expand Up @@ -40,7 +46,7 @@ export default {
],
},
create: function (context) {
const { options, settings, config } = resolveContext(context)
const { options, settings } = resolveContext(context)
const sourceCode = context.sourceCode

const visitNode = (node, arg = null) => {
Expand All @@ -53,8 +59,8 @@ export default {
let expStrings = []

if (arg === null) {
originalClassNamesValue = astUtil.extractValueFromNode(node)
const range = astUtil.extractRangeFromNode(node)
originalClassNamesValue = extractValueFromNode(node)
const range = extractRangeFromNode(node)
if (node.type === 'TextAttribute') {
start = range[0]
end = range[1]
Expand Down Expand Up @@ -128,23 +134,22 @@ export default {
// start/end does not include the backticks, therefore it matches value.raw.
const txt = context.sourceCode.getText(arg)

prefix = astUtil.getTemplateElementPrefix(txt, originalClassNamesValue)
suffix = astUtil.getTemplateElementSuffix(txt, originalClassNamesValue)
originalClassNamesValue = astUtil.getTemplateElementBody(txt, prefix, suffix)
prefix = getTemplateElementPrefix(txt, originalClassNamesValue)
suffix = getTemplateElementSuffix(txt, originalClassNamesValue)
originalClassNamesValue = getTemplateElementBody(txt, prefix, suffix)
break
}
}

const { classNames, whitespaces, headSpace, tailSpace } =
astUtil.extractClassnamesFromValue(originalClassNamesValue)
extractClassnamesFromValue(originalClassNamesValue)

if (classNames.length <= 1) {
// Don't run sorting for a single or empty className
return
}

let orderedClassNames = reorderForReadableClasses(classNames, config)
.filter(eachOrderedClassName => classNames.includes(eachOrderedClassName))
let orderedClassNames = reorderValidClassesAction(classNames, settings.config)

orderedClassNames = classNames.filter(x => !orderedClassNames.includes(x))
.concat(orderedClassNames)
Expand Down Expand Up @@ -178,7 +183,7 @@ export default {
const nodeStartLine = node.loc.start.line
const nodeEndLine = node.loc.end.line
const descriptor = {
loc: astUtil.findLoc(originalClassNamesValue, sourceCodeLines, nodeStartLine, nodeEndLine),
loc: findLoc(originalClassNamesValue, sourceCodeLines, nodeStartLine, nodeEndLine),
messageId: 'invalidClassOrder',
fix: function (fixer) {
return fixer.replaceTextRange([start, end], validatedClassNamesValue)
Expand Down
15 changes: 8 additions & 7 deletions packages/eslint-plugin/src/rules/class-validation.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as astUtil from '../utils/ast'
import defineVisitors from '../utils/define-visitors'
import resolveContext from '../utils/resolve-context'
import { validate } from '@master/css-validator'
import { Rule } from 'eslint'
import findLoc from '../utils/find-loc'
import { parseNodeRecursive } from '../utils/parse-node-recursive'
import validateAction from '../utils/validate-action'

export default {
meta: {
Expand Down Expand Up @@ -40,9 +41,9 @@ export default {
],
},
create: function (context) {
const { options, settings, config } = resolveContext(context)
const { options, settings } = resolveContext(context)
const visitNode = (node, arg = null) => {
astUtil.parseNodeRecursive(
parseNodeRecursive(
node,
arg,
(classNames, node) => {
Expand All @@ -51,20 +52,20 @@ export default {
const nodeStartLine = node.loc.start.line
const nodeEndLine = node.loc.end.line
for (const className of classNames) {
const { isMasterCSS, errors } = validate(className, { config })
const { isMasterCSS, errors } = validateAction(className, settings.config)
if (errors.length > 0) {
for (const error of errors) {
if (isMasterCSS) {
context.report({
loc: astUtil.findLoc(className, sourceCodeLines, nodeStartLine, nodeEndLine),
loc: findLoc(className, sourceCodeLines, nodeStartLine, nodeEndLine),
messageId: 'invalidClass',
data: {
message: error.message + '.',
}
})
} else if (options.disallowUnknownClass) {
context.report({
loc: astUtil.findLoc(className, sourceCodeLines, nodeStartLine, nodeEndLine),
loc: findLoc(className, sourceCodeLines, nodeStartLine, nodeEndLine),
messageId: 'disallowUnknownClass',
data: {
message: `"${className}" is not a valid or known class.`
Expand Down
2 changes: 1 addition & 1 deletion packages/eslint-plugin/src/utils/define-visitors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { RuleListener } from '@typescript-eslint/utils/ts-eslint'
import type { RuleListener } from '@typescript-eslint/utils/dist/ts-eslint'
import { Rule } from 'eslint'

export default function defineVisitors({ context, settings }: { context: Rule.RuleContext, settings: any }, visitNode) {
Expand Down
21 changes: 21 additions & 0 deletions packages/eslint-plugin/src/utils/extract-classnames-from-value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const separatorRegEx = /([\t\n\f\r ]+)/

export default function extractClassnamesFromValue(classStr) {
if (typeof classStr !== 'string') {
return { classNames: [], whitespaces: [], headSpace: false, tailSpace: false }
}
const parts = classStr.split(separatorRegEx)
if (parts[0] === '') {
parts.shift()
}
if (parts[parts.length - 1] === '') {
parts.pop()
}
const headSpace = separatorRegEx.test(parts[0])
const tailSpace = separatorRegEx.test(parts[parts.length - 1])
const isClass = (_, i) => (headSpace ? i % 2 !== 0 : i % 2 === 0)
const isNotClass = (_, i) => (headSpace ? i % 2 === 0 : i % 2 !== 0)
const classNames = parts.filter(isClass)
const whitespaces = parts.filter(isNotClass)
return { classNames: classNames, whitespaces: whitespaces, headSpace: headSpace, tailSpace: tailSpace }
}
11 changes: 11 additions & 0 deletions packages/eslint-plugin/src/utils/extract-range-from-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function extractRangeFromNode(node) {
if (node.type === 'TextAttribute' && node.name === 'class') {
return [node.valueSpan.fullStart.offset, node.valueSpan.end.offset]
}
switch (node.value.type) {
case 'JSXExpressionContainer':
return node.value.expression.range
default:
return node.value.range
}
}
20 changes: 20 additions & 0 deletions packages/eslint-plugin/src/utils/extract-value-from-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

export default function extractValueFromNode(node) {
if (node.type === 'TextAttribute' && node.name === 'class') {
return node.value
}
switch (node.value.type) {
case 'JSXExpressionContainer':
return node.value.expression.value
case 'VExpressionContainer':
switch (node.value.expression.type) {
case 'ArrayExpression':
return node.value.expression.elements
case 'ObjectExpression':
return node.value.expression.properties
}
return node.value.expression.value
default:
return node.value.value
}
}
40 changes: 40 additions & 0 deletions packages/eslint-plugin/src/utils/find-loc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@

export default function findLoc(text, lines, startLine, endLine) {
const targetLines = text.match(/.+(?:\r\n|\n)?/g)

let checkingTargetLine = 0
let resultStart = null
let checking = false

for (let i = startLine; i <= endLine; i++) {
const sourceCodeLine = lines[i - 1]

const index = sourceCodeLine.indexOf(targetLines[checkingTargetLine].replace(/\r\n|\n/, ''))
if (index !== -1) {
if (checkingTargetLine === 0) {
resultStart = {
line: i,
column: index
}
}
if (checkingTargetLine === targetLines.length - 1) {
return {
start: resultStart,
end: {
line: i,
column: index + text.length
}
}
}
checking = true
checkingTargetLine++
} else {
if (checking) {
checking = false
checkingTargetLine = 0
resultStart = null
}
}
}
return null
}
13 changes: 13 additions & 0 deletions packages/eslint-plugin/src/utils/get-mastercss.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import MasterCSS, { Config, config } from '@master/css'
import exploreConfig from 'explore-config'

let currentCSS: MasterCSS
let currentConfig: string | Config

export default function getMasterCSS(config: string | Config) {
if (!currentCSS || currentConfig !== config) {
currentCSS = new MasterCSS(typeof config === 'object' ? config : exploreConfig(config || ''))
currentConfig = config
}
return currentCSS
}
8 changes: 8 additions & 0 deletions packages/eslint-plugin/src/utils/get-template-element-body.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function getTemplateElementBody(text, prefix, suffix) {
let arr = text.split(prefix)
arr.shift()
const body = arr.join(prefix)
arr = body.split(suffix)
arr.pop()
return arr.join(suffix)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

export default function getTemplateElementPrefix(text, raw) {
const idx = text.indexOf(raw)
if (idx === 0) {
return ''
}
return text.split(raw).shift()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

export default function getTemplateElementSuffix(text, raw) {
if (text.indexOf(raw) === -1) {
return ''
}
return text.split(raw).pop()
}
Loading

0 comments on commit e66a680

Please sign in to comment.