release: WRNexusJS 0.2.54
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## 0.2.12
|
||||
|
||||
- Added component-aware prop completions, hover tables, required-prop checks,
|
||||
type mismatch errors, and unknown/invalid-option warnings.
|
||||
|
||||
## 0.2.11
|
||||
|
||||
- Fixed `.wrn` files falling back to Plain Text when a legacy `wire` file
|
||||
|
||||
@@ -94,6 +94,13 @@ realtime chat {
|
||||
}
|
||||
```
|
||||
|
||||
Put `// @required` immediately above a prop when callers must provide it while
|
||||
retaining a typed runtime fallback. `propName = undefined` is also treated as
|
||||
required when no fallback is appropriate. The editor shows required props first
|
||||
in completion lists and reports an error when a component mount omits one. Prop
|
||||
types are inferred from defaults, and string options used in component comparisons
|
||||
are offered as completion choices.
|
||||
|
||||
Embedded language highlighting:
|
||||
|
||||
- `view { ... }` uses HTML highlighting.
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wrnexus",
|
||||
"version": "0.2.11",
|
||||
"version": "0.2.12",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wrnexus",
|
||||
"version": "0.2.11",
|
||||
"version": "0.2.12",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"devDependencies": {
|
||||
"@vscode/vsce": "^3.9.2"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "wrnexus",
|
||||
"displayName": "WRNexus Language Support",
|
||||
"description": "Complete language support for WRNexus .wrn files, including highlighting, formatting, diagnostics, snippets, lifecycle hooks, state watchers, component functions, completions, and definition navigation.",
|
||||
"version": "0.2.11",
|
||||
"version": "0.2.12",
|
||||
"publisher": "wrnexus",
|
||||
"private": true,
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
"use strict";
|
||||
|
||||
const vscode = require("vscode");
|
||||
const {
|
||||
parseComponentMetadata,
|
||||
parseComponentTags,
|
||||
validateComponentTags,
|
||||
} = require("./component-metadata");
|
||||
|
||||
let catalogPromise = null;
|
||||
|
||||
async function readMetadata(uri) {
|
||||
try {
|
||||
const document = await vscode.workspace.openTextDocument(uri);
|
||||
return parseComponentMetadata(document.getText(), uri);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function installedUiUris() {
|
||||
const uris = [];
|
||||
for (const folder of vscode.workspace.workspaceFolders || []) {
|
||||
const directory = vscode.Uri.joinPath(
|
||||
folder.uri,
|
||||
"node_modules",
|
||||
"@wrnexus",
|
||||
"ui",
|
||||
"components",
|
||||
);
|
||||
try {
|
||||
for (const [name, type] of await vscode.workspace.fs.readDirectory(directory)) {
|
||||
if (type === vscode.FileType.File && name.endsWith(".wrn")) {
|
||||
uris.push(vscode.Uri.joinPath(directory, name));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// The UI package is optional; app-local components still work.
|
||||
}
|
||||
}
|
||||
return uris;
|
||||
}
|
||||
|
||||
async function loadComponentCatalog() {
|
||||
const localUris = await vscode.workspace.findFiles(
|
||||
"**/*.wrn",
|
||||
"**/{node_modules,dist,.wrnexus,.git}/**",
|
||||
);
|
||||
const uiUris = await installedUiUris();
|
||||
const metadata = await Promise.all([...uiUris, ...localUris].map(readMetadata));
|
||||
const catalog = new Map();
|
||||
for (const component of metadata) {
|
||||
if (component && (component.kind === "component" || component.kind === "layout")) {
|
||||
catalog.set(component.name, component);
|
||||
}
|
||||
}
|
||||
return catalog;
|
||||
}
|
||||
|
||||
function componentCatalog() {
|
||||
catalogPromise ||= loadComponentCatalog();
|
||||
return catalogPromise;
|
||||
}
|
||||
|
||||
function invalidateCatalog() {
|
||||
catalogPromise = null;
|
||||
}
|
||||
|
||||
function openingTagAt(source, offset) {
|
||||
const opening = source.lastIndexOf("<", offset);
|
||||
const closing = source.lastIndexOf(">", offset);
|
||||
if (opening <= closing) return null;
|
||||
const fragment = source.slice(opening, offset);
|
||||
const match = /^<([A-Z][A-Za-z0-9_$]*)\b/.exec(fragment);
|
||||
return match ? { name: match[1], fragment } : null;
|
||||
}
|
||||
|
||||
function propSnippet(prop) {
|
||||
if (prop.type === "boolean") return `${prop.name}="{\${1:false}}"`;
|
||||
if (prop.type === "number") return `${prop.name}="{\${1:0}}"`;
|
||||
if (prop.type === "array") return `${prop.name}="{[\${1}]}"`;
|
||||
if (prop.type === "object") return `${prop.name}="{{ \${1} }}"`;
|
||||
if (prop.options.length > 0) return `${prop.name}="\${1|${prop.options.join(",")}|}"`;
|
||||
return `${prop.name}="\${1}"`;
|
||||
}
|
||||
|
||||
function propDocumentation(prop) {
|
||||
const lines = [
|
||||
`**${prop.name}** — \`${prop.type}\` ${prop.required ? "**required**" : "optional"}`,
|
||||
];
|
||||
if (!prop.required) lines.push(`Default: \`${prop.defaultValue}\``);
|
||||
if (prop.options.length > 0)
|
||||
lines.push(`Allowed: ${prop.options.map((v) => `\`${v}\``).join(", ")}`);
|
||||
return lines.join("\n\n");
|
||||
}
|
||||
|
||||
async function provideComponentCompletions(document, position) {
|
||||
const source = document.getText();
|
||||
const offset = document.offsetAt(position);
|
||||
const tag = openingTagAt(source, offset);
|
||||
const catalog = await componentCatalog();
|
||||
const items = [];
|
||||
|
||||
if (tag) {
|
||||
const component = catalog.get(tag.name);
|
||||
if (!component) return items;
|
||||
const used = new Set([...tag.fragment.matchAll(/\s([^\s=/>]+)\s*=/g)].map((m) => m[1]));
|
||||
for (const prop of component.props) {
|
||||
if (used.has(prop.name)) continue;
|
||||
const item = new vscode.CompletionItem(prop.name, vscode.CompletionItemKind.Property);
|
||||
item.detail = `${prop.required ? "required" : "optional"} · ${prop.type}`;
|
||||
item.documentation = new vscode.MarkdownString(propDocumentation(prop));
|
||||
item.insertText = new vscode.SnippetString(propSnippet(prop));
|
||||
item.sortText = `${prop.required ? "0" : "1"}-${prop.name}`;
|
||||
items.push(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
const prefix = source.slice(Math.max(0, offset - 80), offset);
|
||||
if (!/<[A-Za-z0-9_$]*$/.test(prefix)) return items;
|
||||
for (const component of catalog.values()) {
|
||||
const item = new vscode.CompletionItem(component.name, vscode.CompletionItemKind.Class);
|
||||
const required = component.props.filter((prop) => prop.required);
|
||||
item.detail = `WRN ${component.kind} · ${component.props.length} props`;
|
||||
item.documentation = new vscode.MarkdownString(componentMarkdown(component));
|
||||
item.insertText = new vscode.SnippetString(
|
||||
`${component.name}${required.map((prop, index) => ` ${prop.name}="\${${index + 1}}"`).join("")} />`,
|
||||
);
|
||||
items.push(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function componentMarkdown(component) {
|
||||
const lines = [
|
||||
`### <${component.name}>`,
|
||||
"",
|
||||
"| Prop | Type | Required | Default / options |",
|
||||
"|---|---|---:|---|",
|
||||
];
|
||||
for (const prop of component.props) {
|
||||
const detail =
|
||||
prop.options.length > 0
|
||||
? prop.options.join(" \\| ")
|
||||
: prop.required
|
||||
? "—"
|
||||
: prop.defaultValue;
|
||||
lines.push(
|
||||
`| \`${prop.name}\` | \`${prop.type}\` | ${prop.required ? "yes" : "no"} | ${detail} |`,
|
||||
);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
async function provideComponentHover(document, position) {
|
||||
const source = document.getText();
|
||||
const offset = document.offsetAt(position);
|
||||
const catalog = await componentCatalog();
|
||||
const tag = parseComponentTags(source).find(
|
||||
(candidate) => offset >= candidate.start && offset <= candidate.end,
|
||||
);
|
||||
if (!tag) return null;
|
||||
const component = catalog.get(tag.name);
|
||||
if (!component) return null;
|
||||
const attribute = tag.attributes.find(
|
||||
(candidate) => offset >= candidate.nameStart && offset <= candidate.nameEnd,
|
||||
);
|
||||
const markdown = attribute
|
||||
? propDocumentation(
|
||||
component.props.find((prop) => prop.name === attribute.name) || {
|
||||
name: attribute.name,
|
||||
type: "unknown",
|
||||
required: false,
|
||||
defaultValue: "unknown",
|
||||
options: [],
|
||||
},
|
||||
)
|
||||
: componentMarkdown(component);
|
||||
return new vscode.Hover(new vscode.MarkdownString(markdown));
|
||||
}
|
||||
|
||||
function registerComponentIntelligence(context) {
|
||||
const diagnostics = vscode.languages.createDiagnosticCollection("wrnexus-components");
|
||||
|
||||
const update = async (document) => {
|
||||
if (document.languageId !== "wrn") return;
|
||||
const catalog = await componentCatalog();
|
||||
const results = validateComponentTags(document.getText(), catalog).map((result) => {
|
||||
const diagnostic = new vscode.Diagnostic(
|
||||
new vscode.Range(document.positionAt(result.start), document.positionAt(result.end)),
|
||||
result.message,
|
||||
result.severity === "error"
|
||||
? vscode.DiagnosticSeverity.Error
|
||||
: vscode.DiagnosticSeverity.Warning,
|
||||
);
|
||||
diagnostic.source = "WRNexus Components";
|
||||
diagnostic.code = result.code;
|
||||
return diagnostic;
|
||||
});
|
||||
diagnostics.set(document.uri, results);
|
||||
};
|
||||
|
||||
const watcher = vscode.workspace.createFileSystemWatcher("**/*.wrn");
|
||||
const refreshOpenDocuments = () => {
|
||||
invalidateCatalog();
|
||||
for (const document of vscode.workspace.textDocuments) void update(document);
|
||||
};
|
||||
|
||||
context.subscriptions.push(
|
||||
diagnostics,
|
||||
watcher,
|
||||
watcher.onDidCreate(refreshOpenDocuments),
|
||||
watcher.onDidChange(refreshOpenDocuments),
|
||||
watcher.onDidDelete(refreshOpenDocuments),
|
||||
vscode.workspace.onDidOpenTextDocument(update),
|
||||
vscode.workspace.onDidChangeTextDocument((event) => void update(event.document)),
|
||||
vscode.workspace.onDidSaveTextDocument((document) => {
|
||||
invalidateCatalog();
|
||||
void update(document);
|
||||
}),
|
||||
vscode.workspace.onDidCloseTextDocument((document) => diagnostics.delete(document.uri)),
|
||||
vscode.languages.registerCompletionItemProvider(
|
||||
{ language: "wrn" },
|
||||
{ provideCompletionItems: provideComponentCompletions },
|
||||
"<",
|
||||
" ",
|
||||
),
|
||||
vscode.languages.registerHoverProvider(
|
||||
{ language: "wrn" },
|
||||
{ provideHover: provideComponentHover },
|
||||
),
|
||||
);
|
||||
|
||||
for (const document of vscode.workspace.textDocuments) void update(document);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
componentMarkdown,
|
||||
openingTagAt,
|
||||
propDocumentation,
|
||||
propSnippet,
|
||||
registerComponentIntelligence,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -9,6 +9,8 @@ const { registerCompletionProvider } = require("./completion");
|
||||
|
||||
const { registerDefinitionProvider } = require("./definition");
|
||||
|
||||
const { registerComponentIntelligence } = require("./component-intelligence");
|
||||
|
||||
const { registerDiagnostics } = require("./diagnostics");
|
||||
|
||||
/**
|
||||
@@ -337,6 +339,7 @@ function activate(context) {
|
||||
registerCompilerDiagnostics(context);
|
||||
registerCompletionProvider(context);
|
||||
registerDefinitionProvider(context);
|
||||
registerComponentIntelligence(context);
|
||||
registerFormatter(context);
|
||||
registerSemanticTokens(context);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert");
|
||||
const { test } = require("node:test");
|
||||
const {
|
||||
attributeValueType,
|
||||
parseComponentMetadata,
|
||||
parseComponentTags,
|
||||
validateComponentTags,
|
||||
} = require("../src/component-metadata");
|
||||
|
||||
const source = `component Button {
|
||||
props {
|
||||
// @required
|
||||
label = ""
|
||||
variant = "primary"
|
||||
disabled = false
|
||||
count = 0
|
||||
items = []
|
||||
}
|
||||
view {
|
||||
<button class="{variant === 'primary' ? 'brand' : variant === 'danger' ? 'danger' : ''}">{label}</button>
|
||||
}
|
||||
}`;
|
||||
|
||||
test("extracts component prop types, required props, defaults, and options", () => {
|
||||
const component = parseComponentMetadata(source);
|
||||
|
||||
assert.equal(component.name, "Button");
|
||||
assert.deepEqual(
|
||||
component.props.map(({ name, type, required, options }) => ({ name, type, required, options })),
|
||||
[
|
||||
{ name: "label", type: "string", required: true, options: [] },
|
||||
{ name: "variant", type: "string", required: false, options: ["danger", "primary"] },
|
||||
{ name: "disabled", type: "boolean", required: false, options: [] },
|
||||
{ name: "count", type: "number", required: false, options: [] },
|
||||
{ name: "items", type: "array", required: false, options: [] },
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("parses multiline component tags and their attribute locations", () => {
|
||||
const page = `<Button
|
||||
label="Save"
|
||||
disabled="{false}"
|
||||
/>`;
|
||||
const [tag] = parseComponentTags(page);
|
||||
|
||||
assert.equal(tag.name, "Button");
|
||||
assert.deepEqual(
|
||||
tag.attributes.map(({ name, value }) => ({ name, value })),
|
||||
[
|
||||
{ name: "label", value: "Save" },
|
||||
{ name: "disabled", value: "{false}" },
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("reports missing, unknown, mismatched, and invalid-option component props", () => {
|
||||
const catalog = new Map([["Button", parseComponentMetadata(source)]]);
|
||||
const diagnostics = validateComponentTags(
|
||||
`<Button variant="quiet" count="many" extra="value" />`,
|
||||
catalog,
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
diagnostics.map(({ code, severity }) => ({ code, severity })),
|
||||
[
|
||||
{ code: "wrn-missing-component-prop", severity: "error" },
|
||||
{ code: "wrn-component-prop-option", severity: "warning" },
|
||||
{ code: "wrn-component-prop-type", severity: "error" },
|
||||
{ code: "wrn-unknown-component-prop", severity: "warning" },
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("recognizes literal attribute value types", () => {
|
||||
assert.equal(attributeValueType("{false}"), "boolean");
|
||||
assert.equal(attributeValueType("{42}"), "number");
|
||||
assert.equal(attributeValueType("{[1, 2]}"), "array");
|
||||
assert.equal(attributeValueType("plain"), "string");
|
||||
});
|
||||
Binary file not shown.
Reference in New Issue
Block a user