From 2f06c65c7c13840cc668e5e6208041d615dfc96d Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Mon, 9 Mar 2026 10:05:18 +0300 Subject: [PATCH] Revert lint plugin to .js (oxlint uses Node.js internally), add new rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- lint/{plugin.ts => plugin.js} | 27 +++++++++++++++++++++++++++ oxlint.json | 13 +++++++++++-- packages/core/src/style-runs.ts | 4 ++-- packages/core/src/tools/analyze.ts | 8 ++++---- 4 files changed, 44 insertions(+), 8 deletions(-) rename lint/{plugin.ts => plugin.js} (83%) diff --git a/lint/plugin.ts b/lint/plugin.js similarity index 83% rename from lint/plugin.ts rename to lint/plugin.js index 1e11d74aa..0d16f8e4e 100644 --- a/lint/plugin.ts +++ b/lint/plugin.js @@ -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, }, } diff --git a/oxlint.json b/oxlint.json index f40110331..a52d3258f 100644 --- a/oxlint.json +++ b/oxlint.json @@ -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": [ { diff --git a/packages/core/src/style-runs.ts b/packages/core/src/style-runs.ts index c44348151..c204ec11d 100644 --- a/packages/core/src/style-runs.ts +++ b/packages/core/src/style-runs.ts @@ -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) diff --git a/packages/core/src/tools/analyze.ts b/packages/core/src/tools/analyze.ts index 45e06d619..b61636ebe 100644 --- a/packages/core/src/tools/analyze.ts +++ b/packages/core/src/tools/analyze.ts @@ -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) } }