From 8476ab2c993f155ba973df5dd74e76e45b637b32 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Fri, 15 May 2026 17:47:40 +0300 Subject: [PATCH] chore(vue): guard browser side effects --- lint/plugin.js | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++ oxlint.json | 1 + 2 files changed, 62 insertions(+) diff --git a/lint/plugin.js b/lint/plugin.js index ec6e3fbdc..f629ed93e 100644 --- a/lint/plugin.js +++ b/lint/plugin.js @@ -388,6 +388,66 @@ const noGeneratedTestIdLiterals = { } } +const noBrowserSideEffectsInVue = { + meta: { + docs: { + description: + 'Disallow direct browser side effects in Vue components — use VueUse, refs, or app services' + } + }, + create(context) { + const file = normalizedFilename(context) + if (!file.endsWith('.vue')) return {} + + function propertyName(property) { + if (property?.type === 'Identifier') return property.name + if (property?.type === 'Literal' && typeof property.value === 'string') return property.value + return null + } + + function objectName(object) { + return object?.type === 'Identifier' ? object.name : null + } + + return { + CallExpression(node) { + const callee = node.callee + if (callee?.type !== 'MemberExpression') return + const object = objectName(callee.object) + const property = propertyName(callee.property) + + if ( + (object === 'window' || object === 'document') && + (property === 'addEventListener' || property === 'removeEventListener') + ) { + context.report({ + node, + message: 'Use VueUse useEventListener() instead of direct browser event listeners in Vue components.' + }) + return + } + + if (object === 'document' && property === 'createElement') { + context.report({ + node, + message: + 'Do not create DOM elements directly in Vue components. Use template refs, components, or an app service.' + }) + } + }, + MemberExpression(node) { + const object = objectName(node.object) + if (object !== 'localStorage' && object !== 'sessionStorage') return + context.report({ + node, + message: + 'Do not access localStorage/sessionStorage directly in Vue components. Use VueUse storage helpers or an app service.' + }) + } + } + } +} + const noDocumentQuerySelectorInVue = { meta: { docs: { @@ -1479,6 +1539,7 @@ const plugin = { 'no-invalid-test-id-attributes': noInvalidTestIdAttributes, 'no-raw-test-id-selectors-in-tests': noRawTestIdSelectorsInTests, 'no-generated-test-id-literals': noGeneratedTestIdLiterals, + 'no-browser-side-effects-in-vue': noBrowserSideEffectsInVue, 'no-document-query-selector-in-vue': noDocumentQuerySelectorInVue, 'no-direct-selection-tool-state-mutation': noDirectSelectionToolStateMutation, 'no-math-random': noMathRandom, diff --git a/oxlint.json b/oxlint.json index 6dce1ae1e..9cda4c5fa 100644 --- a/oxlint.json +++ b/oxlint.json @@ -118,6 +118,7 @@ "open-pencil/no-invalid-test-id-attributes": "error", "open-pencil/no-raw-test-id-selectors-in-tests": "error", "open-pencil/no-generated-test-id-literals": "error", + "open-pencil/no-browser-side-effects-in-vue": "error", "open-pencil/no-document-query-selector-in-vue": "error", "open-pencil/no-direct-selection-tool-state-mutation": "error", "open-pencil/no-math-random": "error",