Revert lint plugin to .js (oxlint uses Node.js internally), add new rules

Oxlint's JS plugin loader uses Node.js, not Bun — .ts fails in CI
with ERR_UNKNOWN_FILE_EXTENSION.

New rules:
- no-hand-rolled-color: flags rgba()/rgb() in template literals outside
  color.ts — use colorToCSS()/colorToHex() helpers instead
- typescript/no-unnecessary-boolean-literal-compare
- typescript/no-unnecessary-template-expression
- typescript/no-unnecessary-type-arguments
- unicorn/no-useless-fallback-in-spread
- unicorn/no-length-as-slice-end
- unicorn/no-unnecessary-await
- unicorn/prefer-string-starts-ends-with

Fixed violations: visible !== false → fill.visible in analyze.ts,
removed ?? {} fallbacks in style-runs.ts spread.
This commit is contained in:
Danila Poyarkov 2026-03-09 10:05:18 +03:00
parent df0ccbca48
commit 2f06c65c7c
4 changed files with 44 additions and 8 deletions

View file

@ -121,12 +121,39 @@ const noMathRandom = {
},
}
const noHandRolledColor = {
meta: {
docs: {
description:
'Disallow hand-rolled color conversions — use helpers from color.ts (colorToCSS, colorToHex, parseColor, etc.)',
},
},
create(context) {
const file = context.filename ?? context.getFilename?.()
if (file?.endsWith('color.ts') || file?.endsWith('color.js')) return {}
return {
TemplateLiteral(node) {
const raw = context.sourceCode.getText(node)
if (/rgba?\s*\(/.test(raw)) {
context.report({
node,
message: "Use colorToCSS() or colorToHex() from color.ts instead of hand-rolled rgba()/rgb() strings.",
})
}
},
}
},
}
const plugin = {
meta: { name: 'open-pencil' },
rules: {
'no-inline-named-types': noInlineNamedTypes,
'no-structuredclone-scene-arrays': noStructuredCloneSceneArrays,
'no-math-random': noMathRandom,
'no-hand-rolled-color': noHandRolledColor,
},
}

View file

@ -1,7 +1,7 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "import", "unicorn", "vue"],
"jsPlugins": ["./lint/plugin.ts"],
"jsPlugins": ["./lint/plugin.js"],
"env": {
"browser": true,
"es2024": true
@ -31,6 +31,9 @@
"typescript/no-unnecessary-condition": "error",
"typescript/no-unnecessary-type-assertion": "error",
"typescript/no-unnecessary-type-parameters": "error",
"typescript/no-unnecessary-boolean-literal-compare": "error",
"typescript/no-unnecessary-template-expression": "error",
"typescript/no-unnecessary-type-arguments": "error",
"typescript/prefer-optional-chain": "error",
"typescript/prefer-for-of": "error",
@ -42,6 +45,11 @@
"unicorn/no-instanceof-array": "error",
"unicorn/no-typeof-undefined": "error",
"unicorn/no-nested-ternary": "error",
"unicorn/no-useless-fallback-in-spread": "error",
"unicorn/no-length-as-slice-end": "error",
"unicorn/no-unnecessary-await": "error",
"unicorn/prefer-string-starts-ends-with": "error",
"unicorn/consistent-existence-index-check": "error",
"vue/no-arrow-functions-in-watch": "error",
@ -66,7 +74,8 @@
"Matrix": "m00:number,m01:number,m02:number,m10:number,m11:number,m12:number"
}],
"open-pencil/no-structuredclone-scene-arrays": "error",
"open-pencil/no-math-random": "error"
"open-pencil/no-math-random": "error",
"open-pencil/no-hand-rolled-color": "error"
},
"overrides": [
{

View file

@ -13,7 +13,7 @@ function expandRuns(runs: StyleRun[], textLength: number): (CharacterStyleOverri
const chars: (CharacterStyleOverride | null)[] = new Array(textLength).fill(null)
for (const run of runs) {
for (let i = run.start; i < run.start + run.length && i < textLength; i++) {
chars[i] = { ...(chars[i] ?? {}), ...run.style }
chars[i] = { ...chars[i], ...run.style }
}
}
return chars
@ -29,7 +29,7 @@ export function applyStyleToRange(
const chars = expandRuns(runs, textLength)
for (let i = start; i < end && i < textLength; i++) {
chars[i] = { ...(chars[i] ?? {}), ...patch }
chars[i] = { ...chars[i], ...patch }
}
return compactRuns(chars)

View file

@ -38,10 +38,10 @@ function serializeNodeProps(raw: SceneNode): string {
lines.push(`size: ${raw.width} ${raw.height}`)
lines.push(`pos: ${raw.x} ${raw.y}`)
const solidFill = raw.fills.find((f) => f.type === 'SOLID' && f.visible !== false)
const solidFill = raw.fills.find((f) => f.type === 'SOLID' && f.visible)
if (solidFill) lines.push(`fill: ${colorToHex(solidFill.color)}`)
const solidStroke = raw.strokes.find((s) => s.visible !== false)
const solidStroke = raw.strokes.find((s) => s.visible)
if (solidStroke) {
lines.push(`stroke: ${colorToHex(solidStroke.color)}`)
if (solidStroke.weight) lines.push(`strokeWeight: ${solidStroke.weight}`)
@ -155,12 +155,12 @@ export const analyzeColors = defineTool({
const boundVars = raw.boundVariables
for (const fill of raw.fills) {
if (fill.type === 'SOLID' && fill.visible !== false) {
if (fill.type === 'SOLID' && fill.visible) {
trackColor(colorMap, fill.color, boundVars?.['fills'] ? String(boundVars['fills']) : null)
}
}
for (const stroke of raw.strokes) {
if (stroke.visible !== false) {
if (stroke.visible) {
trackColor(colorMap, stroke.color, boundVars?.['strokes'] ? String(boundVars['strokes']) : null)
}
}