diff --git a/package.json b/package.json index b95589d36..b5198b93e 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "ts-pnp": "1.2.0", "url": "^0.11.4", "watch": "^1.0.2", + "web-tree-sitter": "0.25.9", "web-vitals": "^1.0.1" }, "scripts": { diff --git a/public/wasm/tree-sitter-python.wasm b/public/wasm/tree-sitter-python.wasm new file mode 100644 index 000000000..dc9de36f0 Binary files /dev/null and b/public/wasm/tree-sitter-python.wasm differ diff --git a/public/wasm/tree-sitter.wasm b/public/wasm/tree-sitter.wasm new file mode 100755 index 000000000..10916b8ec Binary files /dev/null and b/public/wasm/tree-sitter.wasm differ diff --git a/src/assets/stylesheets/EditorPanel.scss b/src/assets/stylesheets/EditorPanel.scss index 7b72ecb83..1dbf8fa50 100644 --- a/src/assets/stylesheets/EditorPanel.scss +++ b/src/assets/stylesheets/EditorPanel.scss @@ -52,3 +52,112 @@ .rpf-alert { margin: 0; } + + +// tree sitter +.problem-marker { + position: relative; + cursor: help; + + &::before { + content: attr(data-error-type); + position: absolute; + display: none; + z-index: 9999; + } + + &::after { + content: attr(data-message); + position: absolute; /* Changed from fixed to absolute for proper positioning */ + display: none; + top: 100%; /* Position below the marker by default */ + left: 0; /* Align with the left of the marker */ + background: #fff; + padding: 6px 10px; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + z-index: 9999; + white-space: pre-wrap; + min-width: 200px; + max-width: 400px; + font-family: sans-serif; + font-size: 13px; + color: #333; + pointer-events: none; + overflow: visible; + max-height: none; + border-left: 4px solid #e91e63; + transform: translateY(5px); /* Small offset for better visibility */ + } + + &:hover::after, + &:focus::after { + display: block; + } + + /* Position tooltip based on available space */ + &[data-line-position="top"]::after { + top: auto; /* Reset top positioning */ + bottom: 100%; /* Position above instead of below */ + transform: translateY(-5px); /* Offset upwards */ + } +} + +.problem-error { + border-bottom: 2px dotted #ff4b4b; +} + +.problem-warning { + border-bottom: 2px dotted #ffb74d; +} + +/* Support for touch devices */ +@media (pointer: coarse) { + .problem-marker { + &::after { + display: none; + } + + &:active::after { + display: block; + } + } +} + +// Tooltip styling +.cm-tooltip { + max-width: 300px; + white-space: pre-wrap; + z-index: 1000; +} + +.cm-tooltip .problem-tooltip { + background: #fff; + border: 1px solid #ccc; + border-radius: 4px; + padding: 8px; + font-size: 13px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +// Loading indicator +.parser-loading { + padding: 10px; + background: #fff3cd; + color: #856404; + margin: 10px 0; + border-radius: 4px; + border: 1px solid #ffeaa7; +} + +// Python parser loader +.python-parser-loader { + padding: 10px; + background: #fff3cd; + color: #856404; + margin: 10px 0; + border-radius: 4px; + text-align: center; + font-weight: 500; +} diff --git a/src/components/Editor/EditorPanel/EditorPanel.jsx b/src/components/Editor/EditorPanel/EditorPanel.jsx index 59d69020c..3ab795f3e 100644 --- a/src/components/Editor/EditorPanel/EditorPanel.jsx +++ b/src/components/Editor/EditorPanel/EditorPanel.jsx @@ -10,7 +10,8 @@ import { useCookies } from "react-cookie"; import { useTranslation } from "react-i18next"; import { basicSetup } from "codemirror"; import { EditorView, keymap } from "@codemirror/view"; -import { EditorState } from "@codemirror/state"; +import { EditorState, StateField } from "@codemirror/state"; +import { Decoration } from "@codemirror/view"; import { defaultKeymap, indentWithTab } from "@codemirror/commands"; import { indentationMarkers } from "@replit/codemirror-indentation-markers"; import { indentUnit } from "@codemirror/language"; @@ -25,6 +26,7 @@ import { Alert } from "@raspberrypifoundation/design-system-react"; import { editorLightTheme } from "../../../assets/themes/editorLightTheme"; import { editorDarkTheme } from "../../../assets/themes/editorDarkTheme"; import { SettingsContext } from "../../../utils/settings"; +import useTreeSitterParser from "../../../hooks/useTreeSitterParser"; const MAX_CHARACTERS = 8500000; @@ -40,6 +42,34 @@ const EditorPanel = ({ extension = "html", fileName = "index" }) => { const settings = useContext(SettingsContext); const [characterLimitExceeded, setCharacterLimitExceeded] = useState(false); + // Use the new Tree Sitter hook + const { + treeSitterParser, + parserInitialized, + analyzePythonCode, + createProblemDecorations, + problemDecorationsEffect, + } = useTreeSitterParser(extension, fileName); + + // Create a state field for problem decorations + const problemDecorationsField = StateField.define({ + create() { + return Decoration.none; + }, + update(decorations, transaction) { + decorations = decorations.map(transaction.changes); + + for (let effect of transaction.effects) { + if (effect.is(problemDecorationsEffect)) { + decorations = effect.value; + } + } + + return decorations; + }, + provide: (f) => EditorView.decorations.from(f), + }); + const updateStoredProject = (content) => { dispatch( updateProjectComponent({ @@ -54,9 +84,34 @@ const EditorPanel = ({ extension = "html", fileName = "index" }) => { const label = EditorView.contentAttributes.of({ "aria-label": t("editorPanel.ariaLabel"), }); + const onUpdate = EditorView.updateListener.of((viewUpdate) => { if (viewUpdate.docChanged) { - updateStoredProject(viewUpdate.state.doc.toString()); + const content = viewUpdate.state.doc.toString(); + updateStoredProject(content); + + // Analyze Python code if applicable + if (extension === "py" && treeSitterParser) { + analyzePythonCode(content).then((problems) => { + // Create decorations for problem areas + if (editorViewRef.current && problems.length > 0) { + const decorationSet = createProblemDecorations( + viewUpdate.state, + problems, + ); + + // Update the editor view with decorations + editorViewRef.current.dispatch({ + effects: [problemDecorationsEffect.of(decorationSet)], + }); + } else if (editorViewRef.current) { + // Clear decorations if no problems + editorViewRef.current.dispatch({ + effects: [problemDecorationsEffect.of(Decoration.none)], + }); + } + }); + } } }); @@ -74,6 +129,7 @@ const EditorPanel = ({ extension = "html", fileName = "index" }) => { return html(); } }; + const isDarkMode = cookies.theme === "dark" || (!cookies.theme && @@ -120,6 +176,7 @@ const EditorPanel = ({ extension = "html", fileName = "index" }) => { indentUnit.of(customIndentUnit), EditorView.editable.of(!readOnly), limitCharacters, + problemDecorationsField, ], }); @@ -141,10 +198,22 @@ const EditorPanel = ({ extension = "html", fileName = "index" }) => { img.setAttribute("role", "presentation"); } + // Initial analysis for Python files when the editor is first created + if (extension === "py" && treeSitterParser && parserInitialized) { + analyzePythonCode(code).then((problems) => { + if (problems.length > 0 && view) { + const decorationSet = createProblemDecorations(view.state, problems); + view.dispatch({ + effects: [problemDecorationsEffect.of(decorationSet)], + }); + } + }); + } + return () => { view.destroy(); }; - }, [cookies]); + }, [cookies, treeSitterParser, parserInitialized]); useEffect(() => { if ( @@ -160,12 +229,36 @@ const EditorPanel = ({ extension = "html", fileName = "index" }) => { }, }); dispatch(setCascadeUpdate(false)); + + // Re-analyze Python code after cascade update + if (extension === "py" && treeSitterParser) { + analyzePythonCode(file.content).then((problems) => { + if (problems.length > 0) { + const decorationSet = createProblemDecorations( + editorViewRef.current.state, + problems, + ); + editorViewRef.current.dispatch({ + effects: [problemDecorationsEffect.of(decorationSet)], + }); + } else { + editorViewRef.current.dispatch({ + effects: [problemDecorationsEffect.of(Decoration.none)], + }); + } + }); + } } - }, [file, cascadeUpdate, editorViewRef]); + }, [file, cascadeUpdate, editorViewRef, treeSitterParser, extension]); return (
+ {extension === "py" && !parserInitialized && ( +
+ {t("editorPanel.pythonSyntaxErrors.loadingParser")} +
+ )} {characterLimitExceeded && ( { + const [treeSitterParser, setTreeSitterParser] = useState(null); + const [parserInitialized, setParserInitialized] = useState(false); + const { t } = useTranslation(); // TODO: use for localization of error messages + + // Create effect for problem decorations + const problemDecorationsEffect = StateEffect.define(); + + // Initialize Tree-sitter parser for Python (WASM version) + useEffect(() => { + if (extension !== "py") { + setParserInitialized(false); + return; + } + + const initTreeSitter = async () => { + try { + await Parser.init({ + locateFile() { + return `${process.env.PUBLIC_URL}/wasm/tree-sitter.wasm`; + }, + }); + + const parser = new Parser(); + const response = await fetch( + `${process.env.PUBLIC_URL}/wasm/tree-sitter-python.wasm`, + ); + const wasmBytes = await response.arrayBuffer(); + const pythonLanguage = await Language.load(new Uint8Array(wasmBytes)); + + await parser.setLanguage(pythonLanguage); + setTreeSitterParser(parser); + setParserInitialized(true); + + console.log( + "Tree-sitter Python parser initialized successfully for", + fileName, + ); + } catch (error) { + console.error("Failed to initialize Tree-sitter:", error); + setParserInitialized(false); + } + }; + + initTreeSitter(); + }, [extension, fileName]); + + function calculateOffsets(lines, startPos, clampedEndPos, errorMessage) { + let errorLine = lines[startPos.row] || ""; + const commentIndex = errorLine.indexOf("#"); + if (commentIndex >= 0) { + errorLine = errorLine.substring(0, commentIndex); + } + + let fromOffset = 0; + for (let i = 0; i < startPos.row; i++) { + fromOffset += lines[i].length + 1; // +1 for newline + } + fromOffset += startPos.column; + + let toOffset = fromOffset; // Default if same position + if ( + startPos.row === clampedEndPos.row && + startPos.column === clampedEndPos.column + ) { + // Zero-width error, extend by one character for visibility + toOffset = fromOffset + 1; + } else { + // Calculate end offset based on clamped position + toOffset = 0; + for (let i = 0; i < clampedEndPos.row; i++) { + toOffset += lines[i].length + 1; + } + toOffset += clampedEndPos.column; + } + + // Default to using the original range + let adjustedFromOffset = fromOffset; + let adjustedToOffset = toOffset; + + // Special handling for "Missing comma between items" errors + if (errorMessage.startsWith("Missing comma between")) { + try { + // Find the pattern of two items with whitespace between them (where comma should be) + let missingCommaRegex; + if (errorMessage.includes("list")) { + missingCommaRegex = /(\w+)\s+(\w+)/g; + } else if (errorMessage.includes("dictionary")) { + missingCommaRegex = /(\w+:\s*\w+)\s+(\w+)/g; + } else { + missingCommaRegex = /(\w+)\s+(\w+)/g; + } + + // Find all matches - we want to find the specific occurrence that's causing the error + const matches = [...errorLine.matchAll(missingCommaRegex)]; + + if (matches.length > 0) { + // Find match closest to error position + let bestMatch = matches[0]; + let bestDistance = Infinity; + + for (const match of matches) { + const matchStart = errorLine.indexOf(match[0]); + const distance = Math.abs(matchStart - startPos.column); + + if (distance < bestDistance) { + bestDistance = distance; + bestMatch = match; + } + } + + // Calculate the position of this match in the original line + const matchStart = errorLine.indexOf(bestMatch[0]); + const matchEnd = matchStart + bestMatch[0].length; + + // Adjust the offsets to highlight just the items and whitespace between them + const lineStartOffset = fromOffset - startPos.column; // Start of the line + adjustedFromOffset = lineStartOffset + matchStart; + adjustedToOffset = lineStartOffset + matchEnd; + + console.log("Adjusted missing comma range:", { + original: { from: fromOffset, to: toOffset }, + adjusted: { from: adjustedFromOffset, to: adjustedToOffset }, + match: bestMatch[0], + }); + } + } catch (e) { + console.error("Error processing missing comma pattern:", e); + // Fall back to default highlighting if regex fails + } + } + + // If the error occurs in a line with comments, make sure we only highlight the code portion + if (commentIndex >= 0) { + const lineStartOffset = fromOffset - startPos.column; // Start of the line + const commentStartOffset = lineStartOffset + commentIndex; + + // Don't extend highlighting into the comment + if (adjustedToOffset > commentStartOffset) { + adjustedToOffset = commentStartOffset; + } + } + return { fromOffset, toOffset, adjustedFromOffset, adjustedToOffset }; + } + + /** + * Analyze Python code for syntax errors using Tree-sitter + * @param {string} code - Python code to analyze + * @returns {Array} Array of detected problems + */ + const analyzePythonCode = async (code) => { + if (!treeSitterParser || extension !== "py") return []; + + try { + const tree = treeSitterParser.parse(code); + const problems = []; + + // Create a query to find all ERROR nodes directly + const queryString = ` + (ERROR) @error + `; + + const query = treeSitterParser.language.query(queryString); + const matches = query.matches(tree.rootNode); + + console.log(`Found ${matches.length} potential syntax errors via query`); + + // Process each matched error node + for (const match of matches) { + const node = match.captures[0].node; + const nodeType = node.type; + const startPos = node.startPosition; + const endPos = node.endPosition; + + // Skip nested errors at the same position + const parent = node.parent; + if ( + parent && + parent.type === "ERROR" && + parent.startPosition.row === startPos.row && + parent.startPosition.column === startPos.column && + parent.endPosition.row === endPos.row && + parent.endPosition.column === endPos.column + ) { + console.log(`Skipping nested ${nodeType} at same position`, startPos); + continue; + } + + // Clamp large error ranges to just the current line plus one + let clampedEndPos = { ...endPos }; + if (endPos.row > startPos.row) { + clampedEndPos = { + row: startPos.row + 1, + column: 0, + }; + console.log( + `Clamping large error range from line ${startPos.row} to ${endPos.row}, now ends at ${clampedEndPos.row}`, + ); + } + + const lines = code.split("\n"); + + const errorMessage = getHelpfulErrorMessage(lines, startPos); + + // Calculate character offsets for CodeMirror + let { fromOffset, toOffset, adjustedFromOffset, adjustedToOffset } = + calculateOffsets(lines, startPos, clampedEndPos, errorMessage); + + problems.push({ + message: errorMessage, + severity: "error", + from: adjustedFromOffset, + to: adjustedToOffset, + context: { + row: startPos.row, + column: startPos.column, + text: code.slice(adjustedFromOffset, adjustedToOffset), + nodeType: nodeType, + }, + }); + + console.log( + `Found ${nodeType} at line ${startPos.row + 1}, column ${ + startPos.column + }`, + { + nodeType: nodeType, + parent: parent?.type, + text: code.slice(adjustedFromOffset, adjustedToOffset), + fromOffset, + toOffset, + adjustedFromOffset, + adjustedToOffset, + }, + ); + } + + // Log problems to console for debugging + if (problems.length > 0) { + console.log("Python code analysis found problems:", problems); + // before returning problems, remove any overlapping ones. Check if on same row, and take the one that is wider + const nonOverlappingProblems = []; + for (const problem of problems) { + // check if same context row + const overlap = nonOverlappingProblems.find( + (p) => p.context.row === problem.context.row, + ); + if (overlap) { + // if the new problem is wider, replace the old one + if (problem.to - problem.from > overlap.to - overlap.from) { + const index = nonOverlappingProblems.indexOf(overlap); + nonOverlappingProblems[index] = problem; + } + } else { + nonOverlappingProblems.push(problem); + } + } + console.log( + "Returning non-overlapping problems:", + nonOverlappingProblems, + ); + return nonOverlappingProblems; + } else { + console.log("No Python syntax errors detected"); + return problems; + } + } catch (error) { + console.error("Error analyzing Python code:", error); + return []; + } + }; + + const getHelpfulErrorMessage = (lines, startPos) => { + // Get the error line text + let errorLine = lines[startPos.row] || ""; + // remove any comments from the line for analysis + errorLine = errorLine.split("#")[0]; + + const nextLine = lines[startPos.row + 1] || ""; + + // Use a much narrower context for error analysis - focus only on the current line and a bit of context + // This helps prevent other errors in the file from contaminating the analysis + const lineErrorContext = errorLine; + + // More focused context that still allows us to detect multi-line issues + // Just a few characters before and after the error position + const narrowErrorContext = [ + startPos.row > 0 ? lines[startPos.row - 1] : "", // Previous line + errorLine, // Error line + nextLine, // Next line + ] + .filter(Boolean) + .join("\n"); + + console.log("Error detection context:", { + line: startPos.row + 1, + errorLine, + narrowContext: narrowErrorContext, + }); + + // Check for common Python syntax errors with more precise contexts + + // 0. Missing operator between values (takes precedence over other checks) + // This matches patterns like "x = 5 6" or "y = a b" where an operator is missing + if ( + /=\s*\w+\s+\d+/.test(lineErrorContext) || + /=\s*\d+\s+\w+/.test(lineErrorContext) || + /=\s*\d+\s+\d+/.test(lineErrorContext) || + /=\s*\w+\s+\w+/.test(lineErrorContext) || + /\w+\s+\d+\s*$/.test(lineErrorContext.trim()) || + /\d+\s+\w+\s*$/.test(lineErrorContext.trim()) || + /\d+\s+\d+\s*$/.test(lineErrorContext.trim()) + ) { + // Check if the error is at the position of the missing operator + return "Missing operator between values (did you mean +, -, *, /, etc.?)"; + } + + // 1. Missing colons - only check the current line and be more specific about patterns + if ( + /^\s*(if|elif|else|for|while|def|class|with|try|except|finally)\b[^:]*$/.test( + errorLine.trim(), + ) || + /^\s*(if|elif|else|for|while|def|class|with|try|except|finally)\b.*[^:][\s]*$/.test( + errorLine.trim(), + ) + ) { + // Determine which statement is missing the colon + const match = errorLine.match( + /^\s*(if|elif|else|for|while|def|class|with|try|except|finally)\b/, + ); + const statement = match ? match[1] : "statement"; + return `Missing colon : after ${statement} statement`; + } + + // 2. Unclosed strings - only check within the current line + const doubleQuotes = (lineErrorContext.match(/"/g) || []).length; + const singleQuotes = (lineErrorContext.match(/'/g) || []).length; + + if (doubleQuotes % 2 !== 0) { + return 'Unclosed string - missing double quote "'; + } + if (singleQuotes % 2 !== 0) { + return "Unclosed string - missing single quote '"; + } + + // 3. Unbalanced parentheses/brackets - use narrow context + // Focus on immediate context around the error to prevent false positives + const openParens = (narrowErrorContext.match(/\(/g) || []).length; + const closeParens = (narrowErrorContext.match(/\)/g) || []).length; + const openBrackets = (narrowErrorContext.match(/\[/g) || []).length; + const closeBrackets = (narrowErrorContext.match(/]/g) || []).length; + const openBraces = (narrowErrorContext.match(/\{/g) || []).length; + const closeBraces = (narrowErrorContext.match(/}/g) || []).length; + + // Check if the error is at the line level with unbalanced brackets + const lineOpenParens = (lineErrorContext.match(/\(/g) || []).length; + const lineCloseParens = (lineErrorContext.match(/\)/g) || []).length; + const lineOpenBrackets = (lineErrorContext.match(/\[/g) || []).length; + const lineCloseBrackets = (lineErrorContext.match(/\]/g) || []).length; + const lineOpenBraces = (lineErrorContext.match(/\{/g) || []).length; + const lineCloseBraces = (lineErrorContext.match(/\}/g) || []).length; + + // Check line level first, then narrow context + if (lineOpenParens > lineCloseParens) { + return "Missing closing parenthesis ) in this line"; + } else if (lineOpenParens < lineCloseParens) { + return "Unexpected closing parenthesis ) in this line, did you mean to open one with ("; + } else if (openParens > closeParens) { + return "Missing closing parenthesis )'"; + } else if (openParens < closeParens) { + return "Unexpected closing parenthesis ), did you mean to open one with ("; + } + + if (lineOpenBrackets > lineCloseBrackets) { + return "Missing closing bracket ']' in this line"; + } else if (lineOpenBrackets < lineCloseBrackets) { + return "Unexpected closing bracket ']' in this line"; + } else if (openBrackets > closeBrackets) { + return "Missing closing bracket ']'"; + } else if (openBrackets < closeBrackets) { + return "Unexpected closing bracket ']'"; + } + + if (lineOpenBraces > lineCloseBraces) { + return "Missing closing brace '}' in this line"; + } else if (lineOpenBraces < lineCloseBraces) { + return "Unexpected closing brace '}' in this line"; + } else if (openBraces > closeBraces) { + return "Missing closing brace '}'"; + } else if (openBraces < closeBraces) { + return "Unexpected closing brace '}'"; + } + + // 5. Missing commas in collections - limit to current line + if (/\[[^\]]*\w+\s+\w+[^\]]*]/.test(lineErrorContext)) { + return "Missing comma between list items"; + } + if (/\{[^}]*\w+\s+\w+[^}]*}/.test(lineErrorContext)) { + return "Missing comma between dictionary items"; + } + if (/\([^)]*\w+\s+\w+[^)]*\)/.test(lineErrorContext)) { + return "Missing comma between items"; + } + + // 6. Invalid Python syntax - using = instead of == for comparison + if (/\s*if\s+\w+\s*=\s*\w+/.test(lineErrorContext)) { + return "Using = for comparison instead of == in condition"; + } + + // 7. Invalid assignment + if (/^\s*\d+\s*=/.test(lineErrorContext)) { + return "Cannot assign to a literal (number)"; + } + + // 8. End with backslash + if (errorLine.trim().endsWith("\\")) { + return "Line ending with backslash \\ - did you mean to continue the line?"; + } + + // 9. Check for print statements with errors (like missing parentheses or quotes) + if (/^\s*print\s+[^(]/.test(lineErrorContext)) { + return "print() function requires parentheses"; + } + + // Default to a more specific message when possible + const syntaxErrorPatterns = { + "def ": "Syntax error in function definition", + "class ": "Syntax error in class definition", + "if ": "Syntax error in conditional statement", + "elif ": "Syntax error in conditional statement", + else: "Syntax error in conditional statement", + "for ": "Syntax error in loop statement", + "while ": "Syntax error in loop statement", + }; + + const trimmedLine = errorLine.trim(); + for (const [pattern, message] of Object.entries(syntaxErrorPatterns)) { + if (trimmedLine.startsWith(pattern)) { + return message; + } + } + + // Default to a generic error message if we can't identify the specific issue + return "Syntax error in Python code"; + }; + + /** + * Create CodeMirror decorations for problem areas + * @param {EditorState} state - Current editor state + * @param {Array} problems - Array of detected problems + * @returns {DecorationSet} Set of decorations to apply to the editor + */ + const createProblemDecorations = (state, problems) => { + const decorations = problems.map((problem) => { + // Create simple class based on severity + let className = "problem-marker"; + console.log("Creating decoration for problem:", problem); + if (problem.severity === "error") { + className += " problem-error"; + } else if (problem.severity === "warning") { + className += " problem-warning"; + } + + // Use the problem ranges directly + const from = problem.from; + const to = problem.to; + + // Get line number for tooltip positioning + const lineNumber = state.doc.lineAt(from).number - 1; // Convert to 0-based + + // Simplify error type to always get the red border + const errorType = "missing-syntax"; // This will always trigger the red left border + + // Use the problem message directly + const friendlyMessage = problem.message; + + // Basic position logic for tooltips + const totalLines = state.doc.lines; + let linePosition = ""; + if (lineNumber >= totalLines - 3) { + linePosition = "top"; + } else if (lineNumber < 2) { + linePosition = "bottom"; + } + + // Create decoration with consistent format + return Decoration.mark({ + class: className, + attributes: { + "data-message": friendlyMessage, + "data-error-type": errorType, + "data-line": lineNumber.toString(), + "data-severity": problem.severity || "error", + ...(linePosition && { "data-line-position": linePosition }), + }, + }).range(from, to); + }); + + return Decoration.set(decorations, true); + }; + + return { + treeSitterParser, + parserInitialized, + analyzePythonCode, + createProblemDecorations, + problemDecorationsEffect, + }; +}; + +export default useTreeSitterParser; diff --git a/webpack.config.js b/webpack.config.js index 536abd2a4..b7c7b6224 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -86,6 +86,8 @@ module.exports = { fallback: { path: require.resolve("path-browserify"), url: require.resolve("url/"), + fs: false, + module: false, }, }, output: { @@ -124,6 +126,13 @@ module.exports = { ) { res.setHeader("Cross-Origin-Resource-Policy", "cross-origin"); } + + // Set MIME type for WASM files + if (req.url.endsWith(".wasm")) { + res.setHeader("Cross-Origin-Resource-Policy", "cross-origin"); + res.setHeader("Content-Type", "application/wasm"); + } + next(); }); return middlewares; diff --git a/yarn.lock b/yarn.lock index 9e82a9be5..319a2efaf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2898,6 +2898,7 @@ __metadata: url: ^0.11.4 url-loader: 4.1.1 watch: ^1.0.2 + web-tree-sitter: 0.25.9 web-vitals: ^1.0.1 webgl-mock-threejs: ^0.0.1 webpack: 5.95.0 @@ -18786,6 +18787,18 @@ __metadata: languageName: node linkType: hard +"web-tree-sitter@npm:0.25.9": + version: 0.25.9 + resolution: "web-tree-sitter@npm:0.25.9" + peerDependencies: + "@types/emscripten": ^1.40.0 + peerDependenciesMeta: + "@types/emscripten": + optional: true + checksum: e07fc82b876ed89286a68c4f2ba8f5308eb10c677f304c2437df0125180690688a5967279e06e1108b3e413d3fc37cf6a3b772bcdb91ef733e354b97f967b319 + languageName: node + linkType: hard + "web-vitals@npm:^1.0.1": version: 1.1.2 resolution: "web-vitals@npm:1.1.2"