Skip to content

Instantly share code, notes, and snippets.

@tmcw
Last active May 28, 2024 13:49
Show Gist options
  • Save tmcw/fefe8b5c0a63b51bc8a303c8a3553fac to your computer and use it in GitHub Desktop.
Save tmcw/fefe8b5c0a63b51bc8a303c8a3553fac to your computer and use it in GitHub Desktop.
CodeMirror extension to detect and fix missing JSX Pragma

We make JSX configuration a per-val setting by requiring a jsxPragma: https://docs.deno.com/runtime/manual/advanced/jsx_dom/jsx

But most code doesn't do that and relies on a project-wide jsx setting. So, to provide better DX for missing JSX pragmas, we use this CodeMirror extension that detects the lack of a pragma and the presence of JSX syntax, by using CodeMirror's existing syntax tree.

import { linter } from "@codemirror/lint";
// This is practically the same as CodeMirror's Diagnostic type
import type { Diagnostic } from "app/worker/types";
import { syntaxTree } from "@codemirror/language";
import { EditorView } from "@codemirror/view";
/**
* Inner method for scanning for bad JSX code, this is exported
* so we can test this code.
*/
export function scanForBadJSX(view: EditorView) {
let hasPragma = false;
const diagnostics: Diagnostic[] = [];
syntaxTree(view.state)
.cursor()
.iterate((node) => {
// "The JSX pragma needs to be in the leading comments of the module"
// according to the docs, so we should be able to count on this
// BlockComment appearing before the JSXElement
//
// Also, it is is required to be a BlockComment, not single line comment,
// so we only scan for those:
// https://mariusschulz.com/blog/per-file-jsx-factories-in-typescript
if (node.name === "BlockComment") {
const commentContent = view.state.sliceDoc(node.from, node.to);
if (commentContent.includes("@jsxImportSource")) {
hasPragma = true;
}
}
// The CodeMirror mode outputs multiple nodes within JSX content,
// like JSXAttribute and such, but we only look for JSXElement
// because scanning for all of the elements will produce overlapping
// error annotations.
if (node.name === "JSXElement" && !hasPragma) {
diagnostics.push({
from: node.from,
to: node.to,
severity: "error",
message: "To use JSX, you need to specify a @jsxImportSource.",
actions: [
{
name: "Use React",
apply(view, _from, _to) {
view.dispatch({
changes: {
from: 0,
insert: `/** @jsxImportSource https://esm.sh/react */\nimport { renderToString } from "npm:react-dom/server";\n\n`,
},
});
},
},
{
name: "Use Preact",
apply(view, _from, _to) {
view.dispatch({
changes: {
from: 0,
insert: `/** @jsxImportSource https://esm.sh/preact */\nimport { render } from "npm:preact-render-to-string";\n\n`,
},
});
},
},
{
name: "Use Hono",
apply(view, _from, _to) {
view.dispatch({
changes: {
from: 0,
insert: `/** @jsxImportSource https://esm.sh/hono@latest/jsx **/\n\n`,
},
});
},
},
],
});
}
});
return diagnostics;
}
/**
* Look for val code that includes JSX elements,
* but does not include a jsxImportSource pragma, and give
* the end-user a quick way to insert the required pragma.
*
* See Deno docs for more information:
* https://docs.deno.com/runtime/manual/advanced/jsx_dom/jsx
*/
export const linterJSX = linter((view): readonly Diagnostic[] => {
return scanForBadJSX(view);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment