Files
fiddle/public/js/linter.js
root 6ca8519250 Add responsive preview, editor themes, template gallery, devtools, and autocomplete
- Device breakpoint toggles (mobile 375px / tablet 768px / desktop 100%)
- Editor theme selector with 6 themes (VS Dark/Light, High Contrast, Monokai, Dracula, GitHub Dark)
- Starter template gallery with 8 pre-built templates (Todo, API Fetch, CSS Animation, etc.)
- Code autocomplete with DOM/React type definitions and snippet completions
- Devtools panels: console, network, elements, performance
- Code formatter (Prettier), diff view, and linter integration
2026-02-27 01:22:16 -06:00

173 lines
4.8 KiB
JavaScript

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();
}