Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
297 changes: 297 additions & 0 deletions eslint-local-rules/index.js
Copy link
Collaborator

@PClmnt PClmnt Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this have any performance overhead?

This is some quite complex code - in terms of what it is and how it has to do it over everything?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

almost definitely

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do a performance test now

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not as bad as I thought it would be actually:

 - Before (rules off): 40.43s
  - After (rules on): 40.48s
  - Delta: +0.05s (~0.1%), effectively within run‑to‑run noise

Going to run some additional tests

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I think this run is probably more realistic:

 - Before (rules off): 22.66s real
  - After (rules on): 23.54s real
  - Delta: +0.88s (~3.9% of ESLint time)

Original file line number Diff line number Diff line change
@@ -1,4 +1,109 @@
const fs = require("fs")
const path = require("path")
const postcss = require("postcss")

const extractStyleBlocks = text => {
const blocks = []
const styleTagRe = /<style\b[^>]*>/gi
let match
while ((match = styleTagRe.exec(text))) {
const startTagEnd = match.index + match[0].length
const endTag = text.indexOf("</style>", startTagEnd)
if (endTag === -1) {
break
}
blocks.push({
css: text.slice(startTagEnd, endTag),
startIndex: startTagEnd,
})
styleTagRe.lastIndex = endTag + "</style>".length
}
return blocks
}

const buildLineOffsets = text => {
const offsets = [0]
for (let i = 0; i < text.length; i += 1) {
if (text[i] === "\n") {
offsets.push(i + 1)
}
}
return offsets
}

const lineColToIndex = (offsets, line, column) => {
if (!line || !column) {
return 0
}
const lineIndex = offsets[line - 1] ?? 0
return lineIndex + Math.max(column - 1, 0)
}

const splitSelectorList = selector => {
const parts = []
let current = ""
let parenDepth = 0
let bracketDepth = 0
let inString = null

for (let i = 0; i < selector.length; i += 1) {
const char = selector[i]

if (inString) {
current += char
if (char === inString && selector[i - 1] !== "\\") {
inString = null
}
continue
}

if (char === "\"" || char === "'") {
inString = char
current += char
continue
}

if (char === "(") {
parenDepth += 1
current += char
continue
}

if (char === ")") {
parenDepth = Math.max(parenDepth - 1, 0)
current += char
continue
}

if (char === "[") {
bracketDepth += 1
current += char
continue
}

if (char === "]") {
bracketDepth = Math.max(bracketDepth - 1, 0)
current += char
continue
}

if (char === "," && parenDepth === 0 && bracketDepth === 0) {
if (current.trim()) {
parts.push(current.trim())
}
current = ""
continue
}

current += char
}

if (current.trim()) {
parts.push(current.trim())
}

return parts
}

const makeBarrelPath = finalPath => {
return path.resolve(__dirname, "..", finalPath)
Expand Down Expand Up @@ -198,4 +303,196 @@ module.exports = {
}
},
},
"no-display-contents-custom-props": {
meta: {
type: "problem",
docs: {
description:
"Warn when display: contents is combined with CSS custom properties in the same selector",
},
schema: [],
messages: {
splitRule:
"Selector \"{{selector}}\" sets display: contents alongside CSS custom properties. Split into separate selectors to avoid CSS tree-shaking.",
},
},
create(context) {
const filename = context.getFilename()
const svelteIndex = filename.lastIndexOf(".svelte")
if (svelteIndex === -1) {
return {}
}

const sourceCode = context.getSourceCode()
const resolvedFilename = filename.slice(0, svelteIndex + ".svelte".length)
const text =
resolvedFilename !== "<input>" && fs.existsSync(resolvedFilename)
? fs.readFileSync(resolvedFilename, "utf8")
: sourceCode.getText()
const styleBlocks = extractStyleBlocks(text)

return {
Program() {
for (const block of styleBlocks) {
let root
try {
root = postcss.parse(block.css)
} catch (error) {
continue
}

const offsets = buildLineOffsets(block.css)

root.walkRules(rule => {
let hasDisplayContents = false
let hasCustomProps = false

rule.walkDecls(decl => {
const prop = decl.prop ? decl.prop.trim() : ""
if (!prop) {
return
}
if (prop.startsWith("--")) {
hasCustomProps = true
}
if (
prop === "display" &&
decl.value &&
decl.value.trim() === "contents"
) {
hasDisplayContents = true
}
})

if (!hasDisplayContents || !hasCustomProps) {
return
}

const start = rule.source?.start
const end = rule.source?.end || start

if (start && end) {
const startIndex =
block.startIndex +
lineColToIndex(offsets, start.line, start.column)
const endIndex =
block.startIndex +
lineColToIndex(offsets, end.line, end.column)

context.report({
node: sourceCode.ast,
loc: {
start: sourceCode.getLocFromIndex(startIndex),
end: sourceCode.getLocFromIndex(endIndex),
},
messageId: "splitRule",
data: { selector: rule.selector || "selector" },
})
return
}

context.report({
node: sourceCode.ast,
messageId: "splitRule",
data: { selector: rule.selector || "selector" },
})
})
}
},
}
},
},
"no-multiple-child-global-selectors": {
meta: {
type: "problem",
docs: {
description:
"Disallow multiple child combinator :global selectors in a comma-separated selector list",
},
schema: [],
messages: {
splitRule:
"Selector list contains multiple child combinator :global selectors. Split into separate rules to avoid minification issues.",
},
},
create(context) {
const filename = context.getFilename()
const svelteIndex = filename.lastIndexOf(".svelte")
if (svelteIndex === -1) {
return {}
}

const sourceCode = context.getSourceCode()
const resolvedFilename = filename.slice(0, svelteIndex + ".svelte".length)
const text =
resolvedFilename !== "<input>" && fs.existsSync(resolvedFilename)
? fs.readFileSync(resolvedFilename, "utf8")
: sourceCode.getText()
const styleBlocks = extractStyleBlocks(text)

return {
Program() {
for (const block of styleBlocks) {
let root
try {
root = postcss.parse(block.css)
} catch (error) {
continue
}

const offsets = buildLineOffsets(block.css)

root.walkRules(rule => {
if (!rule.selector || !rule.selector.includes(",")) {
return
}

const selectors = splitSelectorList(rule.selector)
let childGlobalCount = 0

for (const selector of selectors) {
if (/>\s*:global\(/.test(selector)) {
childGlobalCount += 1
if (childGlobalCount > 1) {
break
}
}
}

if (childGlobalCount <= 1) {
return
}

const start = rule.source?.start
const end = rule.source?.end || start

if (start && end) {
const startIndex =
block.startIndex +
lineColToIndex(offsets, start.line, start.column)
const endIndex =
block.startIndex +
lineColToIndex(offsets, end.line, end.column)

context.report({
node: sourceCode.ast,
loc: {
start: sourceCode.getLocFromIndex(startIndex),
end: sourceCode.getLocFromIndex(endIndex),
},
messageId: "splitRule",
})
return
}

context.report({
node: sourceCode.ast,
messageId: "splitRule",
})
})
}
},
}
},
},
}
2 changes: 2 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export default [
"no-useless-rename": "error",
"no-var": "error",
"no-void": "error",
"local-rules/no-display-contents-custom-props": "error",
"local-rules/no-multiple-child-global-selectors": "error",

"no-unused-vars": [
"error",
Expand Down
2 changes: 2 additions & 0 deletions packages/bbui/src/Table/Table.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,8 @@
--table-border: 1px solid var(--spectrum-alias-border-color-mid);
--cell-padding: var(--spectrum-global-dimension-size-250);
overflow: auto;
}
.wrapper {
display: contents;
}
.wrapper--quiet {
Expand Down
4 changes: 3 additions & 1 deletion packages/builder/src/components/settings/ModalSideBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@

<style>
.modal_sidebar_wrapper {
display: contents;
--nav-logo-width: 20px;
--nav-padding: 12px;
--nav-collapsed-width: calc(
Expand All @@ -107,6 +106,9 @@
--nav-width: 240px;
--nav-border: 1px solid var(--spectrum-global-color-gray-200);
}
.modal_sidebar_wrapper {
display: contents;
}
/* Spacer to allow nav to always be absolutely positioned */
.nav_spacer {
flex: 0 0 var(--nav-collapsed-width);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -643,9 +643,6 @@
}

/* Split into two rules to prevent CSS tree-shaking in production builds */
.nav_wrapper {
display: contents;
}
.nav_wrapper {
--nav-padding: 12px;
--nav-collapsed-width: calc(
Expand All @@ -654,6 +651,9 @@
--nav-width: 240px;
--nav-border: 1px solid var(--spectrum-global-color-gray-200);
}
.nav_wrapper {
display: contents;
}
/* Spacer to allow nav to always be absolutely positioned */
.nav_spacer {
flex: 0 0 var(--nav-collapsed-width);
Expand Down
4 changes: 3 additions & 1 deletion packages/client/src/components/app/pdf/PDFTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,11 @@

<style>
.vars {
display: contents;
--border-color: var(--spectrum-global-color-gray-300);
}
.vars {
display: contents;
}
.table {
display: grid;
grid-template-columns: repeat(var(--cols), minmax(40px, auto));
Expand Down
Loading