lint frontend

This commit is contained in:
2026-03-31 19:52:48 -07:00
parent cd2722f845
commit f905bede92
7 changed files with 2036 additions and 32 deletions

View File

@@ -1,16 +1,21 @@
import js from '@eslint/js'; import js from '@eslint/js';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks'; import reactHooks from 'eslint-plugin-react-hooks';
export default [ export default [
js.configs.recommended, js.configs.recommended,
{ {
files: ['src/**/*.{js,jsx}'], files: ['src/**/*.{js,jsx}'],
plugins: { 'react-hooks': reactHooks }, plugins: {
react,
'react-hooks': reactHooks,
},
languageOptions: { languageOptions: {
ecmaVersion: 'latest', ecmaVersion: 'latest',
sourceType: 'module', sourceType: 'module',
parserOptions: { ecmaFeatures: { jsx: true } }, parserOptions: { ecmaFeatures: { jsx: true } },
globals: { globals: {
// Browser APIs
window: 'readonly', window: 'readonly',
document: 'readonly', document: 'readonly',
console: 'readonly', console: 'readonly',
@@ -33,11 +38,15 @@ export default [
Image: 'readonly', Image: 'readonly',
WebSocket: 'readonly', WebSocket: 'readonly',
HTMLElement: 'readonly', HTMLElement: 'readonly',
Element: 'readonly',
ClipboardItem: 'readonly', ClipboardItem: 'readonly',
CSS: 'readonly', CSS: 'readonly',
ResizeObserver: 'readonly', ResizeObserver: 'readonly',
MutationObserver: 'readonly', MutationObserver: 'readonly',
IntersectionObserver: 'readonly', IntersectionObserver: 'readonly',
TextEncoder: 'readonly',
TextDecoder: 'readonly',
Buffer: 'readonly',
atob: 'readonly', atob: 'readonly',
btoa: 'readonly', btoa: 'readonly',
performance: 'readonly', performance: 'readonly',
@@ -45,6 +54,9 @@ export default [
queueMicrotask: 'readonly', queueMicrotask: 'readonly',
}, },
}, },
settings: {
react: { version: 'detect' },
},
rules: { rules: {
// Prevent the TDZ bug // Prevent the TDZ bug
'no-use-before-define': ['error', { functions: false, classes: false, variables: true }], 'no-use-before-define': ['error', { functions: false, classes: false, variables: true }],
@@ -53,10 +65,24 @@ export default [
'react-hooks/rules-of-hooks': 'error', 'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn', 'react-hooks/exhaustive-deps': 'warn',
// React JSX correctness
'react/jsx-key': 'error',
'react/jsx-no-duplicate-props': 'error',
'react/no-children-prop': 'error',
'react/no-danger-with-children': 'error',
'react/no-direct-mutation-state': 'error',
'react/no-unescaped-entities': 'warn',
// Turn off rules that are noisy without adding safety // Turn off rules that are noisy without adding safety
'react/react-in-jsx-scope': 'off', // not needed with React 17+ JSX transform
'react/prop-types': 'off', // no PropTypes in this codebase
'react/no-unknown-property': 'off', // false positives with Three.js / custom attrs
'no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], 'no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
'no-empty': 'off', 'no-empty': 'off',
'no-prototype-builtins': 'off', 'no-prototype-builtins': 'off',
}, },
linterOptions: {
reportUnusedDisableDirectives: 'off',
},
}, },
]; ];

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,7 @@
"@vitejs/plugin-react": "^4.3.0", "@vitejs/plugin-react": "^4.3.0",
"c8": "^10.1.3", "c8": "^10.1.3",
"eslint": "^9.39.4", "eslint": "^9.39.4",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"vite": "^5.4.0" "vite": "^5.4.0"
} }

View File

@@ -877,6 +877,11 @@ function Flow() {
const journalContentRef = useRef(''); const journalContentRef = useRef('');
const reactFlow = useReactFlow(); const reactFlow = useReactFlow();
const scheduleAutoRun = useCallback(() => {
clearTimeout(autoRunTimer.current);
autoRunTimer.current = setTimeout(() => autoRunRef.current?.(), 300);
}, []);
// ── WebSocket ─────────────────────────────────────────────────────── // ── WebSocket ───────────────────────────────────────────────────────
const updateNodeData = useCallback((nodeId, patch) => { const updateNodeData = useCallback((nodeId, patch) => {
@@ -1648,6 +1653,14 @@ function Flow() {
}); });
}, [reactFlow]); }, [reactFlow]);
const openJournalTab = useCallback(() => {
setHelpTabs((prev) => {
if (prev.find((t) => t.label === 'Journal')) return prev;
return [...prev, { label: 'Journal', type: 'journal', content: journalContentRef.current }];
});
setActiveHelpTab('Journal');
}, []);
// ── Add node from context menu ────────────────────────────────────── // ── Add node from context menu ──────────────────────────────────────
const addNode = useCallback((className, def) => { const addNode = useCallback((className, def) => {
@@ -1797,11 +1810,6 @@ function Flow() {
}); });
}; };
const scheduleAutoRun = useCallback(() => {
clearTimeout(autoRunTimer.current);
autoRunTimer.current = setTimeout(() => autoRunRef.current?.(), 300);
}, []);
const onRuntimeValuesChange = useCallback((nodeId, patch, { scheduleRun = false } = {}) => { const onRuntimeValuesChange = useCallback((nodeId, patch, { scheduleRun = false } = {}) => {
if (!patch || typeof patch !== 'object') return; if (!patch || typeof patch !== 'object') return;
@@ -1958,14 +1966,6 @@ function Flow() {
}); });
}, []); }, []);
const openJournalTab = useCallback(() => {
setHelpTabs((prev) => {
if (prev.find((t) => t.label === 'Journal')) return prev;
return [...prev, { label: 'Journal', type: 'journal', content: journalContentRef.current }];
});
setActiveHelpTab('Journal');
}, []);
const updateTabContent = useCallback((label, content) => { const updateTabContent = useCallback((label, content) => {
if (label === 'Journal') journalContentRef.current = content; if (label === 'Journal') journalContentRef.current = content;
setHelpTabs((prev) => prev.map((t) => t.label === label ? { ...t, content } : t)); setHelpTabs((prev) => prev.map((t) => t.label === label ? { ...t, content } : t));

View File

@@ -980,13 +980,6 @@ function NodeTable({ rows }) {
function CustomNode({ id, data }) { function CustomNode({ id, data }) {
const ctx = useContext(NodeContext); const ctx = useContext(NodeContext);
if (data.className === 'Group') {
return <GroupNode id={id} data={data} />;
}
if (data.className === 'TextNote') {
return <TextNoteNode id={id} data={data} />;
}
const def = data.definition; const def = data.definition;
const scalarDisplay = formatScalarDisplay(data.scalarValue); const scalarDisplay = formatScalarDisplay(data.scalarValue);
const processingTimeText = formatProcessingTime(data.processingTimeMs); const processingTimeText = formatProcessingTime(data.processingTimeMs);
@@ -996,6 +989,7 @@ function CustomNode({ id, data }) {
// Find the COORDPAIR input name (if any) so we can resolve live upstream positions // Find the COORDPAIR input name (if any) so we can resolve live upstream positions
const coordPairInputName = React.useMemo(() => { const coordPairInputName = React.useMemo(() => {
if (!def) return null;
const allInputs = { ...def.input.required, ...def.input.optional }; const allInputs = { ...def.input.required, ...def.input.optional };
for (const [name, spec] of Object.entries(allInputs)) { for (const [name, spec] of Object.entries(allInputs)) {
const type = Array.isArray(spec) ? spec[0] : spec; const type = Array.isArray(spec) ? spec[0] : spec;
@@ -1018,8 +1012,8 @@ function CustomNode({ id, data }) {
); );
// Parse inputs into data handles and widgets // Parse inputs into data handles and widgets
const required = def.input.required || {}; const required = def?.input?.required || {};
const optional = def.input.optional || {}; const optional = def?.input?.optional || {};
const dataInputs = []; const dataInputs = [];
const widgets = []; const widgets = [];
@@ -1044,7 +1038,7 @@ function CustomNode({ id, data }) {
// For manual-trigger nodes (Save), show progressive optional inputs: // For manual-trigger nodes (Save), show progressive optional inputs:
// show field_N only if field_(N-1) is connected (or N==0). // show field_N only if field_(N-1) is connected (or N==0).
const isProgressive = def.manual_trigger; const isProgressive = def?.manual_trigger;
const connectedInputs = useStore( const connectedInputs = useStore(
useCallback( useCallback(
(s) => { (s) => {
@@ -1075,6 +1069,13 @@ function CustomNode({ id, data }) {
), ),
); );
if (data.className === 'Group') {
return <GroupNode id={id} data={data} />;
}
if (data.className === 'TextNote') {
return <TextNoteNode id={id} data={data} />;
}
for (const [name, spec] of Object.entries(optional)) { for (const [name, spec] of Object.entries(optional)) {
const [type, opts] = getSpecTypeAndOptions(spec); const [type, opts] = getSpecTypeAndOptions(spec);
if (isProgressive && isDataSocketSpec(spec)) { if (isProgressive && isDataSocketSpec(spec)) {
@@ -1210,7 +1211,7 @@ function CustomNode({ id, data }) {
style={{ background: TYPE_COLORS[socketType] || 'var(--fallback-type)' }} style={{ background: TYPE_COLORS[socketType] || 'var(--fallback-type)' }}
/> />
)} )}
{!!( {(
(w.socketType && connectedInputs?.has(w.name)) (w.socketType && connectedInputs?.has(w.name))
|| (combinedInputName && connectedInputs?.has(combinedInputName)) || (combinedInputName && connectedInputs?.has(combinedInputName))
) ? ( ) ? (
@@ -1556,7 +1557,6 @@ function TextInputValueBox({ val, placeholder, nodeId, name, label, hideLabel, o
> >
{editing ? ( {editing ? (
<input <input
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus autoFocus
className="nodrag" className="nodrag"
type="text" type="text"

View File

@@ -11,7 +11,7 @@ function parseHeadings(md) {
for (const line of lines) { for (const line of lines) {
const m = line.match(/^(#{1,6})\s+(.+)/); const m = line.match(/^(#{1,6})\s+(.+)/);
if (m) { if (m) {
const text = m[2].replace(/[*_`~\[\]]/g, '').trim(); const text = m[2].replace(/[*_`~[\]]/g, '').trim();
const id = text.toLowerCase().replace(/[^\w]+/g, '-').replace(/(^-|-$)/g, ''); const id = text.toLowerCase().replace(/[^\w]+/g, '-').replace(/(^-|-$)/g, '');
headings.push({ level: m[1].length, text, id }); headings.push({ level: m[1].length, text, id });
} }
@@ -187,7 +187,6 @@ function JournalTab({ content, onChange, onOpenDoc }) {
} }
}} }}
placeholder="Write your notes here (Markdown supported)…" placeholder="Write your notes here (Markdown supported)…"
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus autoFocus
/> />
) : renderedHtml ? ( ) : renderedHtml ? (

View File

@@ -7,6 +7,10 @@ const SI_PREFIX_MULTIPLIERS = {
const NUMBER_WITH_UNIT_RE = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)\s*(.*)?$/; const NUMBER_WITH_UNIT_RE = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)\s*(.*)?$/;
const PREFIXABLE_UNITS = new Set([
'm', 's', 'A', 'V', 'W', 'Hz', 'F', 'C', 'J', 'N', 'Pa', 'T', 'H', 'S', 'g', 'K', 'Ohm', 'ohm', 'Ω',
]);
/** /**
* Parse a string like "1.5 nm" into { numeric: 1.5e-9, unit: "m" }. * Parse a string like "1.5 nm" into { numeric: 1.5e-9, unit: "m" }.
* Returns null if the string does not start with a valid number. * Returns null if the string does not start with a valid number.
@@ -56,10 +60,6 @@ const SI_PREFIXES = [
{ exp: 24, prefix: 'Y' }, { exp: 24, prefix: 'Y' },
]; ];
const PREFIXABLE_UNITS = new Set([
'm', 's', 'A', 'V', 'W', 'Hz', 'F', 'C', 'J', 'N', 'Pa', 'T', 'H', 'S', 'g', 'K', 'Ohm', 'ohm', 'Ω',
]);
const SUPERSCRIPT_DIGITS = { const SUPERSCRIPT_DIGITS = {
'-': '⁻', '-': '⁻',
'0': '⁰', '0': '⁰',