import { loadScript } from './utils.js'; import { getActiveEditor, getActiveTab, getCurrentMode, setOnTabSwitch, setOnModeChange } from './editors.js'; const ESLINT_CDN = 'https://cdn.jsdelivr.net/npm/eslint-linter-browserify@9/linter.min.js'; let linterInstance = null; let loaded = false; let debounceTimer = null; async function ensureESLint() { if (loaded) return; await loadScript(ESLINT_CDN); if (window.eslint && window.eslint.Linter) { linterInstance = new window.eslint.Linter(); } else if (window.Linter) { linterInstance = new window.Linter(); } loaded = true; } function isLintableMode(mode) { return ['html-css-js', 'typescript', 'react', 'react-ts', 'wasm'].includes(mode); } function isLintableTab(tabId) { return tabId === 'js'; } const BROWSER_GLOBALS = { document: 'readonly', window: 'readonly', console: 'readonly', setTimeout: 'readonly', clearTimeout: 'readonly', setInterval: 'readonly', clearInterval: 'readonly', requestAnimationFrame: 'readonly', cancelAnimationFrame: 'readonly', fetch: 'readonly', URL: 'readonly', URLSearchParams: 'readonly', HTMLElement: 'readonly', Event: 'readonly', CustomEvent: 'readonly', MutationObserver: 'readonly', IntersectionObserver: 'readonly', ResizeObserver: 'readonly', localStorage: 'readonly', sessionStorage: 'readonly', navigator: 'readonly', location: 'readonly', history: 'readonly', performance: 'readonly', alert: 'readonly', confirm: 'readonly', prompt: 'readonly', WebAssembly: 'readonly', Uint8Array: 'readonly', ArrayBuffer: 'readonly', Map: 'readonly', Set: 'readonly', WeakMap: 'readonly', WeakSet: 'readonly', Promise: 'readonly', Symbol: 'readonly', Proxy: 'readonly', Reflect: 'readonly', globalThis: 'readonly', self: 'readonly', queueMicrotask: 'readonly', }; const REACT_GLOBALS = { React: 'readonly', ReactDOM: 'readonly', }; function getEslintConfig(mode) { const parserOptions = { ecmaVersion: 'latest', sourceType: 'module', }; const globals = { ...BROWSER_GLOBALS }; if (mode === 'react' || mode === 'react-ts') { parserOptions.ecmaFeatures = { jsx: true }; Object.assign(globals, REACT_GLOBALS); } return { languageOptions: { globals, parserOptions }, rules: { 'no-constant-condition': 'warn', 'no-debugger': 'warn', 'no-empty': 'warn', 'no-extra-semi': 'warn', 'no-unreachable': 'warn', 'no-duplicate-case': 'warn', }, }; } function severityToMonaco(severity) { // ESLint: 1=warn, 2=error if (severity === 2) return monaco.MarkerSeverity.Error; return monaco.MarkerSeverity.Warning; } function lintEditor(editor, mode) { if (!linterInstance || !editor) return 0; const model = editor.getModel(); if (!model) return 0; const code = editor.getValue(); if (!code.trim()) { monaco.editor.setModelMarkers(model, 'eslint', []); return 0; } try { const config = getEslintConfig(mode); const messages = linterInstance.verify(code, config); const markers = messages.map(m => ({ startLineNumber: m.line || 1, startColumn: m.column || 1, endLineNumber: m.endLine || m.line || 1, endColumn: m.endColumn || (m.column || 1) + 1, message: `${m.message} (${m.ruleId || 'syntax'})`, severity: severityToMonaco(m.severity), source: 'eslint', })); monaco.editor.setModelMarkers(model, 'eslint', markers); return markers.length; } catch (e) { monaco.editor.setModelMarkers(model, 'eslint', []); return 0; } } function updateBadge(count) { // Lint feedback is shown as editor squiggles only — no badge needed } function scheduleLint() { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { const mode = getCurrentMode(); const tabId = getActiveTab(); if (!isLintableMode(mode) || !isLintableTab(tabId)) { clearMarkers(); updateBadge(0); return; } const editor = getActiveEditor(); const count = lintEditor(editor, mode); updateBadge(count); }, 800); } function clearMarkers() { const editor = getActiveEditor(); if (editor && editor.getModel()) { monaco.editor.setModelMarkers(editor.getModel(), 'eslint', []); } } export async function initLinter() { try { await ensureESLint(); } catch (e) { console.warn('ESLint failed to load:', e.message); return; } if (!linterInstance) return; // Lint on tab switch setOnTabSwitch((tabId, editor) => { const mode = getCurrentMode(); if (isLintableMode(mode) && isLintableTab(tabId) && editor) { const count = lintEditor(editor, mode); updateBadge(count); } else { updateBadge(0); } }); // Clear markers on mode change setOnModeChange((mode) => { if (!isLintableMode(mode)) { updateBadge(0); } }); } export function lintOnChange() { scheduleLint(); }