2025-05-27 04:00:34 +00:00

451 lines
13 KiB
JavaScript

import { ESLint } from "eslint";
export default defineEventHandler(async (event) => {
const body = await readBody(event);
try {
if (body.code === undefined) {
return {
statusCode: 400,
message: "Bad Request",
};
}
const code = body.code;
// Extract script and template content once
const scriptContent =
code.match(/<script\b[^>]*>([\s\S]*?)<\/script>/)?.[1] || "";
const templateContent = code.match(/<template>([\s\S]*)<\/template>/)?.[1];
// Validate FormKit inputs
const validateFormKit = (content) => {
// List of valid FormKit input types
const validFormKitTypes = [
"text",
"email",
"url",
"tel",
"password",
"number",
"date",
"datetime-local",
"time",
"month",
"week",
"search",
"color",
"file",
"range",
"checkbox",
"radio",
"select",
"textarea",
"submit",
"button",
"mask",
"form",
];
// Find all FormKit components
const formKitRegex = /<FormKit[^>]*>/g;
let formKitMatch;
// Start counting from template tag
let lineNumber = content
.slice(0, content.indexOf("<template"))
.split("\n").length;
let lastIndex = 0;
while ((formKitMatch = formKitRegex.exec(content)) !== null) {
// Calculate correct line number including the lines before template
lineNumber += (
content.slice(lastIndex, formKitMatch.index).match(/\n/g) || []
).length;
lastIndex = formKitMatch.index;
const formKitTag = formKitMatch[0];
// Extract type attribute
const typeMatch = formKitTag.match(/type=["']([^"']+)["']/);
if (!typeMatch) {
throw {
message: "FormKit component missing required 'type' attribute",
line: lineNumber,
column:
formKitMatch.index -
content.lastIndexOf("\n", formKitMatch.index),
};
}
const inputType = typeMatch[1];
if (!validFormKitTypes.includes(inputType)) {
throw {
message: `Invalid FormKit type: "${inputType}". Please use a valid input type.`,
line: lineNumber,
column:
formKitMatch.index -
content.lastIndexOf("\n", formKitMatch.index),
};
}
// Check for options in select, radio, and checkbox types
if (["select", "radio", "checkbox"].includes(inputType)) {
// Look for :options or v-model
const hasOptions =
formKitTag.includes(":options=") || formKitTag.includes("v-model=");
const hasSlotContent =
content
.slice(
formKitMatch.index,
content.indexOf(">", formKitMatch.index)
)
.includes(">") &&
content
.slice(
formKitMatch.index,
content.indexOf("</FormKit>", formKitMatch.index)
)
.includes("<option");
if (!hasOptions && !hasSlotContent) {
throw {
message: `FormKit ${inputType} requires options. Add :options prop or option slots.`,
line: lineNumber,
column:
formKitMatch.index -
content.lastIndexOf("\n", formKitMatch.index),
};
}
}
}
};
// Add new function to validate mustache syntax
const validateMustacheSyntax = (content) => {
const stack = [];
let lineNumber = 1;
let lastIndex = 0;
for (let i = 0; i < content.length; i++) {
if (content[i] === "\n") {
lineNumber++;
lastIndex = i + 1;
}
if (content[i] === "{" && content[i + 1] === "{") {
stack.push({
position: i,
line: lineNumber,
column: i - lastIndex,
});
i++; // Skip next '{'
} else if (content[i] === "}" && content[i + 1] === "}") {
if (stack.length === 0) {
throw {
message:
"Unexpected closing mustache brackets '}}' without matching opening brackets",
line: lineNumber,
column: i - lastIndex,
};
}
stack.pop();
i++; // Skip next '}'
}
}
if (stack.length > 0) {
const unclosed = stack[0];
throw {
message:
"Unclosed mustache brackets '{{'. Missing closing brackets '}}",
line: unclosed.line,
column: unclosed.column,
};
}
};
// Check template content and FormKit validation
if (templateContent) {
try {
validateMustacheSyntax(templateContent);
validateFormKit(templateContent);
} catch (error) {
return {
statusCode: 400,
message: "Template Syntax Error",
data: {
message: error.message,
line: error.line,
column: error.column,
},
};
}
// Check for undefined variables
const definedVariables = new Set();
// Add common Vue variables
const commonVueVars = [
"$route",
"$router",
"$refs",
"$emit",
"$slots",
"$attrs",
];
commonVueVars.forEach((v) => definedVariables.add(v));
// Extract refs and other variables from script
const refRegex = /(?:const|let|var)\s+(\w+)\s*=/g;
let varMatch;
while ((varMatch = refRegex.exec(scriptContent)) !== null) {
definedVariables.add(varMatch[1]);
}
// Extract defineProps if any
const propsMatch = scriptContent.match(/defineProps\(\s*{([^}]+)}\s*\)/);
if (propsMatch) {
const propsContent = propsMatch[1];
const propNames = propsContent.match(/(\w+)\s*:/g);
propNames?.forEach((prop) => {
definedVariables.add(prop.replace(":", "").trim());
});
}
// Check template for undefined variables
const mustacheRegex = /{{([^}]+)}}/g;
let lineNumber = 1;
let lastIndex = 0;
let mustacheMatch;
while ((mustacheMatch = mustacheRegex.exec(templateContent)) !== null) {
// Calculate line number
lineNumber += (
templateContent.slice(lastIndex, mustacheMatch.index).match(/\n/g) ||
[]
).length;
lastIndex = mustacheMatch.index;
const expression = mustacheMatch[1].trim();
// Split expression and check each variable
const variables = expression.split(/[\s.()[\]]+/);
for (const variable of variables) {
// Skip numbers, operators, and empty strings
if (
!variable ||
variable.match(/^[\d+\-*/&|!%<>=?:]+$/) ||
variable === "true" ||
variable === "false"
) {
continue;
}
if (!definedVariables.has(variable)) {
return {
statusCode: 400,
message: "Template Reference Error",
data: {
message: `Variable "${variable}" is not defined`,
line: lineNumber,
column:
mustacheMatch.index -
templateContent.lastIndexOf("\n", mustacheMatch.index),
},
};
}
}
}
}
// Validate template structure
const validateTemplateStructure = (code) => {
// Add new validation for script tags inside template
const templateContent1 = code.match(
/<template>([\s\S]*)<\/template>/
)?.[1];
if (templateContent1) {
const scriptInTemplate = templateContent1.match(/<script\b[^>]*>/i);
if (scriptInTemplate) {
const lineNumber = templateContent1
.slice(0, scriptInTemplate.index)
.split("\n").length;
const column =
scriptInTemplate.index -
templateContent1.lastIndexOf("\n", scriptInTemplate.index);
throw {
message: "Script tags are not allowed inside template section",
line: lineNumber,
column: column,
};
}
}
// Check for root level template and script tags
const rootTemplateCount = (
code.match(/^[\s\S]*<template>[\s\S]*<\/template>/g) || []
).length;
const rootScriptCount = (
code.match(/^[\s\S]*<script>[\s\S]*<\/script>/g) || []
).length;
if (rootTemplateCount > 1 || rootScriptCount > 1) {
throw new Error(
"Vue components must have only one root <template> and one <script> tag"
);
}
// Extract template content for further validation
const templateContent2 = code.match(
/<template>([\s\S]*)<\/template>/
)?.[1];
if (templateContent2) {
const tagStack = [];
const tagRegex = /<\/?([a-zA-Z][a-zA-Z0-9:-]*)\s*([^>]*?)(\/?)>/g;
let match;
let lineNumber = 1;
let lastIndex = 0;
while ((match = tagRegex.exec(templateContent2)) !== null) {
const [fullTag, tagName, attributes, selfClosing] = match;
// Calculate line number
lineNumber += (
templateContent2.slice(lastIndex, match.index).match(/\n/g) || []
).length;
lastIndex = match.index;
// Skip comments
if (templateContent2.slice(match.index).startsWith("<!--")) {
const commentEnd = templateContent2.indexOf("-->", match.index);
if (commentEnd !== -1) {
tagRegex.lastIndex = commentEnd + 3;
continue;
}
}
if (!fullTag.endsWith(">")) {
throw {
message: `Malformed tag found: ${fullTag}`,
line: lineNumber,
column:
match.index - templateContent2.lastIndexOf("\n", match.index),
};
}
if (selfClosing || fullTag.endsWith("/>")) continue;
if (!fullTag.startsWith("</")) {
tagStack.push({
name: tagName,
line: lineNumber,
column:
match.index - templateContent2.lastIndexOf("\n", match.index),
});
} else {
if (tagStack.length === 0) {
throw {
message: `Unexpected closing tag </${tagName}> found without matching opening tag`,
line: lineNumber,
column:
match.index - templateContent2.lastIndexOf("\n", match.index),
};
}
const lastTag = tagStack[tagStack.length - 1];
if (lastTag.name !== tagName) {
throw {
message: `Mismatched tags: expected closing tag for "${lastTag.name}" but found "${tagName}"`,
line: lineNumber,
column:
match.index - templateContent2.lastIndexOf("\n", match.index),
};
}
tagStack.pop();
}
}
if (tagStack.length > 0) {
const unclosedTag = tagStack[tagStack.length - 1];
throw {
message: `Unclosed tag: ${unclosedTag.name}`,
line: unclosedTag.line,
column: unclosedTag.column,
};
}
}
return true;
};
try {
validateTemplateStructure(code);
} catch (structureError) {
return {
statusCode: 400,
message: "Template Structure Error",
data: {
message: structureError.message,
line: structureError.line || 1,
column: structureError.column || 0,
},
};
}
// ESLint configuration
const eslint = new ESLint({
overrideConfig: {
extends: ["plugin:vue/vue3-recommended"],
parserOptions: {
parser: "espree",
ecmaVersion: 2022,
sourceType: "module",
},
},
useEslintrc: false,
});
const results = await eslint.lintText(code);
if (results[0].messages.length > 0) {
const message = results[0].messages[0];
if (message.fatal === true) {
return {
statusCode: 400,
message: "Bad Linter Test",
data: {
message: message.message,
line: message.line,
column: message.column,
},
};
}
return {
statusCode: 200,
message: "Good Linter test",
data: {
message: message.message,
line: message.line,
column: message.column,
},
};
}
return {
statusCode: 200,
message: "Code validation passed",
};
} catch (error) {
console.log(error);
return {
statusCode: 500,
message: "Internal Server Error",
error: error.message,
};
}
});