release: WRNexusJS 0.2.54
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
"use strict";
|
||||
|
||||
const COMPONENT_DECLARATION = /\b(component|layout)\s+([A-Za-z_$][\w$]*)\s*\{/;
|
||||
|
||||
function findMatchingBrace(source, openingBrace) {
|
||||
let depth = 0;
|
||||
let quote = null;
|
||||
let escaped = false;
|
||||
|
||||
for (let index = openingBrace; index < source.length; index += 1) {
|
||||
const character = source[index];
|
||||
if (quote) {
|
||||
if (escaped) escaped = false;
|
||||
else if (character === "\\") escaped = true;
|
||||
else if (character === quote) quote = null;
|
||||
continue;
|
||||
}
|
||||
if (character === '"' || character === "'" || character === "`") quote = character;
|
||||
else if (character === "{") depth += 1;
|
||||
else if (character === "}" && --depth === 0) return index;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
function inferType(defaultValue) {
|
||||
const value = defaultValue.trim();
|
||||
if (value === "undefined") return "unknown";
|
||||
if (/^(?:true|false)$/.test(value)) return "boolean";
|
||||
if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(value)) return "number";
|
||||
if (/^["'`]/.test(value)) return "string";
|
||||
if (value.startsWith("[")) return "array";
|
||||
if (value.startsWith("{")) return "object";
|
||||
if (value === "null") return "null";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function stringLiteral(value) {
|
||||
const match = /^(?:"([\s\S]*)"|'([\s\S]*)'|`([\s\S]*)`)$/.exec(value.trim());
|
||||
return match ? (match[1] ?? match[2] ?? match[3]) : null;
|
||||
}
|
||||
|
||||
function inferOptions(source, propName) {
|
||||
const escaped = propName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const options = new Set();
|
||||
const comparisons = new RegExp(`\\b${escaped}\\s*(?:===|!==|==|!=)\\s*(["'])(.*?)\\1`, "g");
|
||||
let match;
|
||||
while ((match = comparisons.exec(source)) !== null) options.add(match[2]);
|
||||
return [...options].sort();
|
||||
}
|
||||
|
||||
function parseComponentMetadata(source, uri = null) {
|
||||
const declaration = COMPONENT_DECLARATION.exec(source);
|
||||
if (!declaration) return null;
|
||||
|
||||
const propsKeyword = /\bprops\s*\{/.exec(source.slice(declaration.index));
|
||||
const props = [];
|
||||
if (propsKeyword) {
|
||||
const start = declaration.index + propsKeyword.index;
|
||||
const openingBrace = source.indexOf("{", start);
|
||||
const closingBrace = findMatchingBrace(source, openingBrace);
|
||||
const bodyEnd = closingBrace === -1 ? source.length : closingBrace;
|
||||
const body = source.slice(openingBrace + 1, bodyEnd);
|
||||
const linePattern = /^(?:\s*\/\/\s*@required\s*\r?\n)?\s*([A-Za-z_$][\w$]*)\s*=\s*(.*?)\s*$/gm;
|
||||
let propMatch;
|
||||
while ((propMatch = linePattern.exec(body)) !== null) {
|
||||
const defaultValue = propMatch[2];
|
||||
const name = propMatch[1];
|
||||
props.push({
|
||||
name,
|
||||
defaultValue,
|
||||
required: defaultValue.trim() === "undefined" || /^\s*\/\/\s*@required/m.test(propMatch[0]),
|
||||
type: inferType(defaultValue),
|
||||
options: inferOptions(source, name),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { kind: declaration[1], name: declaration[2], props, uri };
|
||||
}
|
||||
|
||||
function unwrapAttributeValue(value) {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.startsWith("{") && trimmed.endsWith("}")) return trimmed.slice(1, -1).trim();
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function attributeValueType(value) {
|
||||
const unwrapped = unwrapAttributeValue(value);
|
||||
if (/^(?:true|false)$/.test(unwrapped)) return "boolean";
|
||||
if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(unwrapped)) return "number";
|
||||
if (unwrapped.startsWith("[")) return "array";
|
||||
if (unwrapped.startsWith("{")) return "object";
|
||||
return "string";
|
||||
}
|
||||
|
||||
function isTypeCompatible(prop, value) {
|
||||
if (prop.type === "unknown" || prop.type === "null") return true;
|
||||
const actual = attributeValueType(value);
|
||||
if (prop.type === actual) return true;
|
||||
if (prop.type === "number" && actual === "string") return Number.isFinite(Number(value));
|
||||
if (prop.type === "boolean" && actual === "string")
|
||||
return /^(?:true|false|1|0|yes|no|on|off)?$/i.test(value);
|
||||
return false;
|
||||
}
|
||||
|
||||
function parseComponentTags(source) {
|
||||
const tags = [];
|
||||
const pattern = /<([A-Z][A-Za-z0-9_$]*)(\s[\s\S]*?)?\s*\/?>/g;
|
||||
let match;
|
||||
while ((match = pattern.exec(source)) !== null) {
|
||||
const attributes = [];
|
||||
const attributeSource = match[2] || "";
|
||||
const attributeOffset = match.index + match[0].indexOf(attributeSource);
|
||||
const attributePattern = /([^\s=/>]+)\s*=\s*(["'])([\s\S]*?)\2/g;
|
||||
let attributeMatch;
|
||||
while ((attributeMatch = attributePattern.exec(attributeSource)) !== null) {
|
||||
const nameStart = attributeOffset + attributeMatch.index;
|
||||
attributes.push({
|
||||
name: attributeMatch[1],
|
||||
value: attributeMatch[3],
|
||||
nameStart,
|
||||
nameEnd: nameStart + attributeMatch[1].length,
|
||||
});
|
||||
}
|
||||
tags.push({
|
||||
name: match[1],
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
nameStart: match.index + 1,
|
||||
nameEnd: match.index + 1 + match[1].length,
|
||||
attributes,
|
||||
});
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
function validateComponentTags(source, components) {
|
||||
const diagnostics = [];
|
||||
for (const tag of parseComponentTags(source)) {
|
||||
const component = components.get(tag.name);
|
||||
if (!component) continue;
|
||||
const provided = new Map(tag.attributes.map((attribute) => [attribute.name, attribute]));
|
||||
const declared = new Map(component.props.map((prop) => [prop.name, prop]));
|
||||
|
||||
for (const prop of component.props) {
|
||||
if (prop.required && !provided.has(prop.name)) {
|
||||
diagnostics.push({
|
||||
severity: "error",
|
||||
code: "wrn-missing-component-prop",
|
||||
message: `<${tag.name}> requires prop \`${prop.name}\` (${prop.type}).`,
|
||||
start: tag.nameStart,
|
||||
end: tag.nameEnd,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const attribute of tag.attributes) {
|
||||
const prop = declared.get(attribute.name);
|
||||
if (!prop) {
|
||||
diagnostics.push({
|
||||
severity: "warning",
|
||||
code: "wrn-unknown-component-prop",
|
||||
message: `Unknown prop \`${attribute.name}\` on <${tag.name}>.`,
|
||||
start: attribute.nameStart,
|
||||
end: attribute.nameEnd,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (!isTypeCompatible(prop, attribute.value)) {
|
||||
diagnostics.push({
|
||||
severity: "error",
|
||||
code: "wrn-component-prop-type",
|
||||
message: `Prop \`${attribute.name}\` on <${tag.name}> expects ${prop.type}, but received ${attributeValueType(attribute.value)}.`,
|
||||
start: attribute.nameStart,
|
||||
end: attribute.nameEnd,
|
||||
});
|
||||
}
|
||||
const literal = stringLiteral(attribute.value) ?? attribute.value;
|
||||
if (prop.options.length > 0 && !prop.options.includes(literal)) {
|
||||
diagnostics.push({
|
||||
severity: "warning",
|
||||
code: "wrn-component-prop-option",
|
||||
message: `Prop \`${attribute.name}\` should be one of: ${prop.options.join(", ")}.`,
|
||||
start: attribute.nameStart,
|
||||
end: attribute.nameEnd,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
attributeValueType,
|
||||
inferType,
|
||||
isTypeCompatible,
|
||||
parseComponentMetadata,
|
||||
parseComponentTags,
|
||||
validateComponentTags,
|
||||
};
|
||||
Reference in New Issue
Block a user