the code editor stops underlining correct code

Not spell check — Monaco's own TypeScript worker, misconfigured in two
independent ways. Both are visible in `monaco.contribution.js:272`, where the
defaults are `{ allowNonTsExtensions: true, target: Latest }` and
`{ noSemanticValidation: false }`.

── 1. It was reading JSX as TypeScript ──

`tsWorker.js:93` decides a file's script kind from the SUFFIX of its model URI:
`ts`/`tsx`/`js`/`jsx`, and otherwise falls back to TS. `<Editor>` was given no
`path`, so its model was `inmemory://model/1` — no dot, no suffix, TS. JSX is
illegal in a `.ts` file, so every tag in the file was a syntax error. That is
the wall-to-wall squiggling in the screenshot.

Passing the real path gives the model a `.tsx` URI. `jsx: ReactJSX` is set too;
both are needed, since the extension alone still errors without the option.

── 2. It was type-checking against a file system it cannot see ──

Semantic validation is now off, and this is the part worth stating plainly.
Monaco runs in the browser; the files are on the server behind the file API.
There is no `node_modules` to resolve `react` from, no `tsconfig.json` to read
paths out of, no way to follow a relative import. So every import resolves to
nothing — which is where "Cannot find module './logo.svg'" came from. Not a
mistake in the file: a checker with no file system.

Wrong errors are worse than none. They teach you to ignore the colour, and then
the one real problem underlined the same way goes unread. Monaco's own authors
already made this call for JavaScript — `javascriptDefaults` ships with
`noSemanticValidation: true`. This extends it to TypeScript for the same reason.

Syntactic validation stays ON: an unclosed brace needs only the buffer in front
of it, and with the extension finally right it is answered correctly.

Real semantic checking would mean shipping the project's dependency types into
the browser — a language-server-over-websocket feature, not a config change.

tsgo clean, frontend builds, 860 pass / 7 fail unchanged. Not verified in a
browser: I cannot see the editor render.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 02:36:56 +00:00
co-authored by Claude Opus 5
parent e2f7a3f091
commit 2b70a5700e
2 changed files with 74 additions and 0 deletions
@@ -12,6 +12,7 @@ import { getLanguage } from './language-map';
import { useFilesRefresh } from '../../channels';
import { useWorkspace } from '../../components/Workspace';
import { useEditorTabs } from './use-editor-tabs';
import { configureMonacoTypescript } from './monaco-setup';
type CodeEditorViewProps = {
className?: string;
@@ -286,10 +287,15 @@ export const CodeEditorView = ({
{activeFile ? (
<Editor
key={activeFile.path}
// Not decoration: the model's URI is where TypeScript learns the file EXTENSION, and the
// extension is what decides whether JSX is legal. Without it every tag in a `.tsx` file
// was underlined as a syntax error. See `monaco-setup.ts`.
path={activeFile.path}
theme={theme}
language={getLanguage(activeFile.name)}
value={activeFile.content}
onChange={(value) => setContent(activeFile.path, value ?? '')}
beforeMount={configureMonacoTypescript}
onMount={handleEditorMount}
options={{
automaticLayout: true,
@@ -0,0 +1,68 @@
import type { Monaco } from '@monaco-editor/react';
/**
* Teach Monaco's TypeScript worker what it can and cannot know here.
*
* Out of the box the editor underlined most of a `.tsx` file in red. Two separate causes, and neither is
* a real defect in the code being edited:
*
* ── 1. It was reading JSX as TypeScript ──
*
* With no `path`, `<Editor>` names its model `inmemory://model/1`. TypeScript decides whether JSX is even
* legal from the file EXTENSION, and that URI has none — so every `<Card>` in the file was a syntax error.
* That is the wall-to-wall squiggling. Fixed by giving the model its real path (see `CodeEditor.tsx`) and
* by turning JSX on below; both are needed, since `.tsx` without the compiler option is still an error.
*
* ── 2. It was type-checking against a file system it cannot see ──
*
* Semantic checking is off, and this is the part worth being honest about. Monaco runs in the browser. The
* files it is editing are on the server, reached through the file API — there is no `node_modules` to
* resolve `react` from, no `tsconfig.json` to read paths out of, and no way to follow a relative import to
* a sibling file. So EVERY import resolves to nothing, which is where "Cannot find module './logo.svg'"
* came from: not a mistake in the file, a checker with no file system.
*
* Wrong errors are worse than no errors. They train you to ignore the colour, and then the one real
* problem underlined the same way goes unread.
*
* Syntactic checking stays ON, because that needs only the text in front of it: an unclosed brace really
* is an unclosed brace. It is the subset of "is this correct" that is answerable from a single buffer, and
* once the extension is right it is answered correctly.
*
* Making semantic checking work would mean shipping the project's dependency types into the browser, which
* is a language-server-over-websocket feature, not a config change.
*/
// Monaco is a singleton behind the loader; configuring it twice is harmless but pointless.
let configured = false;
export function configureMonacoTypescript(monaco: Monaco): void {
if (configured) return;
configured = true;
const ts = monaco.languages.typescript;
const compilerOptions = {
target: ts.ScriptTarget.ESNext,
module: ts.ModuleKind.ESNext,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
jsx: ts.JsxEmit.ReactJSX,
allowJs: true,
allowNonTsExtensions: true,
esModuleInterop: true,
// Nothing is emitted from here — this worker exists only to parse and highlight.
noEmit: true,
};
ts.typescriptDefaults.setCompilerOptions(compilerOptions);
ts.javascriptDefaults.setCompilerOptions(compilerOptions);
// See above: no file system, so no honest semantic answer. Syntax is still checked.
const diagnostics = { noSemanticValidation: true, noSyntaxValidation: false, noSuggestionDiagnostics: true };
ts.typescriptDefaults.setDiagnosticsOptions(diagnostics);
ts.javascriptDefaults.setDiagnosticsOptions(diagnostics);
// The worker would otherwise only see the file that is open, and a `.d.ts` it cannot fetch is a request
// that fails on every keystroke.
ts.typescriptDefaults.setEagerModelSync(true);
ts.javascriptDefaults.setEagerModelSync(true);
}