release: WRNexusJS 0.5.10

This commit is contained in:
2026-07-30 13:36:29 +05:30
parent 8fc6f15402
commit d1b0c55b53
159 changed files with 7509 additions and 604 deletions
+10
View File
@@ -1,5 +1,15 @@
# Changelog
## 0.5.1
- Updated compiler-backed diagnostics for native array and object state values.
- Added support for unquoted component prop expressions such as `items={items}`,
inline arrays, and inline objects.
- Added readable, idempotent formatting for native JSON state values and direct
structured component props.
- Added extension release validation to prevent the bundled compiler from
drifting behind the framework parser.
## 0.3.0
- Added language support for `runtime`, `hydrate`, `computed`, `effect`, `security`,
+1 -1
View File
@@ -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.5.0",
"version": "0.5.1",
"publisher": "wrnexus",
"private": true,
"license": "SEE LICENSE IN LICENSE",
+251 -23
View File
@@ -3,6 +3,142 @@
const __nodeRequire = require;
const __path = __nodeRequire("node:path");
const __modules = {
"packages/compiler/src/cache.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DependencyGraph = void 0;
exports.compilationKey = compilationKey;
exports.createCompilationCache = createCompilationCache;
const node_crypto_1 = require("node:crypto");
const syntax_1 = require("@wrnexus/syntax");
const codegen_ts_1 = require("./codegen.js");
function compileSource(source, filePath) {
const richDiagnostics = (0, syntax_1.diagnose)(source, { file: filePath, accessibility: true });
const errors = richDiagnostics.filter((diagnostic) => diagnostic.severity === "error");
if (errors.length > 0) {
throw new syntax_1.ParseError(errors.map((diagnostic) => diagnostic.message).join("\n"), errors[0].code);
}
const ast = (0, syntax_1.parse)(source);
return {
code: `// compiled from .wrn\n${(0, codegen_ts_1.generate)(ast)}`,
ast,
diagnostics: richDiagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`),
richDiagnostics,
};
}
function compilationKey(source, file = "<inline .wrn>", salt = "") {
return (0, node_crypto_1.createHash)("sha256")
.update(file)
.update("\0")
.update(salt)
.update("\0")
.update(source)
.digest("hex");
}
function createCompilationCache(options = {}) {
const maxEntries = options.maxEntries ?? 500;
if (!Number.isInteger(maxEntries) || maxEntries < 1)
throw new RangeError("maxEntries must be positive");
const now = options.now ?? Date.now;
const entries = new Map();
let hits = 0;
let misses = 0;
function touch(key, value) {
entries.delete(key);
entries.set(key, value);
while (entries.size > maxEntries)
entries.delete(entries.keys().next().value);
}
return {
compile(source, file = "<inline .wrn>", salt = "") {
const key = compilationKey(source, file, salt);
const existing = entries.get(key);
if (existing) {
hits++;
touch(key, existing);
return existing;
}
misses++;
const result = compileSource(source, file);
const entry = {
...result,
key,
file,
sourceHash: (0, node_crypto_1.createHash)("sha256").update(source).digest("hex"),
createdAt: now(),
};
touch(key, entry);
return entry;
},
get(key) {
const entry = entries.get(key);
if (entry)
touch(key, entry);
return entry;
},
invalidate(file) {
let removed = 0;
for (const [key, entry] of entries) {
if (!file || entry.file === file) {
entries.delete(key);
removed++;
}
}
return removed;
},
clear() {
entries.clear();
},
size: () => entries.size,
stats: () => ({ hits, misses, entries: entries.size }),
};
}
class DependencyGraph {
#dependencies = new Map();
#dependents = new Map();
set(file, dependencies) {
this.remove(file);
const values = new Set(dependencies);
this.#dependencies.set(file, values);
for (const dependency of values) {
const set = this.#dependents.get(dependency) ?? new Set();
set.add(file);
this.#dependents.set(dependency, set);
}
}
remove(file) {
for (const dependency of this.#dependencies.get(file) ?? []) {
const set = this.#dependents.get(dependency);
set?.delete(file);
if (set?.size === 0)
this.#dependents.delete(dependency);
}
this.#dependencies.delete(file);
}
dependencies(file) {
return [...(this.#dependencies.get(file) ?? [])].sort();
}
dependents(file) {
return [...(this.#dependents.get(file) ?? [])].sort();
}
affected(file) {
const found = new Set();
const queue = [file];
while (queue.length) {
const current = queue.shift();
for (const dependent of this.#dependents.get(current) ?? []) {
if (found.has(dependent))
continue;
found.add(dependent);
queue.push(dependent);
}
}
return [...found].sort();
}
}
exports.DependencyGraph = DependencyGraph;
},
"packages/compiler/src/codegen.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
/**
@@ -937,6 +1073,9 @@ function exprRefsState(expr, stateNames) {
}
return false;
}
function exprRefsComponentReactiveValue(expr, ctx) {
return exprRefsState(expr, ctx.stateNames) || exprRefsState(expr, ctx.functionNames);
}
function viewHasEvents(nodes) {
return nodes.some((node) => {
if (node.type === "text")
@@ -999,7 +1138,7 @@ function compileText(raw, ctx) {
// list renderer fills it per item; it has no server-side value.
out += escLit(`{${expr}}`);
}
else if (exprRefsState(expr, ctx.stateNames)) {
else if (exprRefsComponentReactiveValue(expr, ctx)) {
// State interpolation: bake the initial value AND keep it reactive via a
// data-text span, so no-JS clients see the real value and hydration
// updates it in place. `count` → `<span data-text="count">0</span>`.
@@ -1156,6 +1295,7 @@ function renderComponentNode(node, ctx) {
});
}
}
const isExplicitComponentMount = node.attrs.some((attribute) => attribute.name === "data-component");
const attrs = node.attrs
.filter((a) => a.name !== "class" && !a.name.startsWith("class:"))
.map((a) => {
@@ -1172,7 +1312,7 @@ function renderComponentNode(node, ctx) {
if (isHtmlBooleanAttribute(a.name)) {
const expression = wholeAttributeExpression(a.value);
if (expression) {
const referencesState = exprRefsState(a.value, ctx.stateNames);
const referencesState = exprRefsComponentReactiveValue(a.value, ctx);
const referencesLoopVariable = elementContext.loopVars
? exprRefsState(a.value, elementContext.loopVars)
: false;
@@ -1189,8 +1329,12 @@ function renderComponentNode(node, ctx) {
if (a.value === "true" || a.value === "")
return ` ${a.name}`;
}
const rendered = ` ${a.name}="${compileAttrValue(a.value, elementContext)}"`;
const referencesState = exprRefsState(a.value, ctx.stateNames);
const wholeExpression = wholeAttributeExpression(a.value);
const compiledValue = isExplicitComponentMount && wholeExpression
? `\${__wireProp(${elementContext.resolveExpr(wholeExpression)})}`
: compileAttrValue(a.value, elementContext);
const rendered = ` ${a.name}="${compiledValue}"`;
const referencesState = exprRefsComponentReactiveValue(a.value, ctx);
const referencesLoopVariable = elementContext.loopVars
? exprRefsState(a.value, elementContext.loopVars)
: false;
@@ -1219,7 +1363,7 @@ function renderComponentNode(node, ctx) {
})
.join("");
const staticClassValue = staticClasses.join(" ");
const classReferencesState = exprRefsState(staticClassValue, ctx.stateNames);
const classReferencesState = exprRefsComponentReactiveValue(staticClassValue, ctx);
const classReferencesLoopVariable = elementContext.loopVars
? exprRefsState(staticClassValue, elementContext.loopVars)
: false;
@@ -1243,9 +1387,7 @@ function renderComponentNode(node, ctx) {
const loopLocalsAttribute = serverLoopLocalsAttribute(ctx);
const allAttrs = `${loopLocalsAttribute}` +
`${ctx.forwardRestAttrs ? "${__wireSpreadAttrs(__attrs)}" : ""}` +
`${ctx.forwardRestAttrs && ctx.eventNames?.length
? ` data-wrn-events="${attrEscape(ctx.eventNames.join(","))}"`
: ""}` +
`${ctx.eventNames?.length ? ` data-wrn-events="${attrEscape(ctx.eventNames.join(","))}"` : ""}` +
`${classAttribute}` +
`${classReactiveBinding}` +
`${classBindings}` +
@@ -1261,9 +1403,6 @@ function generateComponent(ast) {
if (ast.imports.length > 0)
out.push(ast.imports.join("\n"));
const hasServerEach = viewHasServerEach(ast.view);
if (hasServerEach) {
out.push(`import { Buffer } from "node:buffer";`);
}
const effectiveProps = ast.kind === "layout" && !ast.props.some((prop) => prop.name === "content")
? [
{
@@ -1300,6 +1439,9 @@ function generateComponent(ast) {
};
const ctx = {
stateNames,
functionNames: new Set(ast.functions.flatMap((body) => {
return Array.from(body.matchAll(/(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/g), (match) => match[1]);
})),
resolveExpr,
eventNames: ast.events.map((event) => event.name),
};
@@ -1330,6 +1472,9 @@ function generateComponent(ast) {
ast.computed.length > 0 ||
viewHasEvents(ast.view) ||
behavior !== null);
if (hasServerEach || needsScope) {
out.push(`import { Buffer as __WrnexusBuffer } from "node:buffer";`);
}
const scopeKeys = [
...effectiveProps.map((prop) => prop.name),
...ast.states.map((state) => state.name),
@@ -1354,16 +1499,16 @@ function generateComponent(ast) {
const returnExpr = needsScope
? "`" +
styleTag +
`<div data-scope="\${__scope}"${behaviorAttr}${hydrationAttribute(ast)}>` +
`<div data-scope="\${__scope}" data-wrn-scope="\${__scopePayload}"${behaviorAttr}${hydrationAttribute(ast)}>` +
viewCode +
"</div>`"
: "`" + styleTag + viewCode + "`";
const scopeLine = needsScope && scopeKeys.length > 0
? ` const __scope = __wrnexusScopeDecl({ ${scopeKeys
? ` const __scopeState = { ${scopeKeys
.map((key) => `${JSON.stringify(key)}: ${nameRefs.get(key)}`)
.join(", ")} });\n`
.join(", ")} };\n const __scope = __wrnexusScopeDecl(__scopeState);\n const __scopePayload = __WrnexusBuffer.from(JSON.stringify(__scopeState), "utf8").toString("base64");\n`
: needsScope
? ` const __scope = "";\n`
? ` const __scopeState = {};\n const __scope = "";\n const __scopePayload = __WrnexusBuffer.from("{}", "utf8").toString("base64");\n`
: "";
if (ast.kind === "layout") {
out.push(`export const __wrnexusLayout = ${JSON.stringify(ast.name)};`);
@@ -1553,7 +1698,7 @@ function __wireRaw(v: any): string {
}`);
if (hasServerEach) {
out.push(`function __wrnexusEncodeLoopLocals(value: Record<string, any>): string {
return Buffer.from(JSON.stringify(value), "utf8").toString("base64");
return __WrnexusBuffer.from(JSON.stringify(value), "utf8").toString("base64");
}`);
}
if (needsScope) {
@@ -1661,7 +1806,7 @@ function renderPageComponentAttr(attr, dynamicExpressions) {
* `@wrnexus/syntax` package. This package owns platform-specific codegen.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.runtimeTypeOf = exports.inferredRuntimeType = exports.eraseFunctionTypes = exports.LexError = exports.Lexer = exports.NativeCompileError = exports.generateNative = exports.generate = exports.ParseError = exports.parse = exports.formatDiagnostic = exports.diagnosticFromError = exports.diagnose = exports.assertValidAst = void 0;
exports.DependencyGraph = exports.createCompilationCache = exports.compilationKey = exports.runtimeTypeOf = exports.inferredRuntimeType = exports.eraseFunctionTypes = exports.LexError = exports.Lexer = exports.NativeCompileError = exports.generateNative = exports.generate = exports.ParseError = exports.parse = exports.formatDiagnostic = exports.diagnosticFromError = exports.diagnose = exports.assertValidAst = void 0;
exports.compileNativeWireFile = compileNativeWireFile;
exports.compileWireFile = compileWireFile;
exports.compile = compile;
@@ -1725,6 +1870,10 @@ function compile(source, filePath = "<inline .wrn>") {
richDiagnostics,
};
}
var cache_ts_1 = require("./cache.js");
Object.defineProperty(exports, "compilationKey", { enumerable: true, get: function () { return cache_ts_1.compilationKey; } });
Object.defineProperty(exports, "createCompilationCache", { enumerable: true, get: function () { return cache_ts_1.createCompilationCache; } });
Object.defineProperty(exports, "DependencyGraph", { enumerable: true, get: function () { return cache_ts_1.DependencyGraph; } });
},
"packages/compiler/src/native-codegen.ts": function (module, exports, require, __filename, __dirname) {
@@ -2211,7 +2360,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.positionAt = exports.isRuntimeTarget = exports.isHydrationStrategy = exports.formatDiagnostic = exports.diagnosticFromError = exports.diagnose = exports.classifyParseError = exports.assertValidAst = exports.validateTypedInitializer = exports.runtimeTypeOf = exports.inferredRuntimeType = exports.eraseFunctionTypes = exports.VOID_ELEMENTS = exports.ParseError = exports.parseHtmlView = exports.parse = exports.LexError = exports.Lexer = void 0;
exports.supportsSyntaxFeature = exports.diagnosticSummary = exports.sliceSource = exports.createSourceRange = exports.WRN_SYNTAX_FEATURES = exports.WRN_SYNTAX_VERSION = exports.positionAt = exports.isRuntimeTarget = exports.isHydrationStrategy = exports.formatDiagnostic = exports.diagnosticFromError = exports.diagnose = exports.classifyParseError = exports.assertValidAst = exports.validateTypedInitializer = exports.runtimeTypeOf = exports.inferredRuntimeType = exports.eraseFunctionTypes = exports.VOID_ELEMENTS = exports.ParseError = exports.parseHtmlView = exports.parse = exports.LexError = exports.Lexer = void 0;
var tokenizer_ts_1 = require("./tokenizer.js");
Object.defineProperty(exports, "Lexer", { enumerable: true, get: function () { return tokenizer_ts_1.Lexer; } });
Object.defineProperty(exports, "LexError", { enumerable: true, get: function () { return tokenizer_ts_1.LexError; } });
@@ -2235,6 +2384,13 @@ Object.defineProperty(exports, "isHydrationStrategy", { enumerable: true, get: f
Object.defineProperty(exports, "isRuntimeTarget", { enumerable: true, get: function () { return diagnostics_ts_1.isRuntimeTarget; } });
Object.defineProperty(exports, "positionAt", { enumerable: true, get: function () { return diagnostics_ts_1.positionAt; } });
__exportStar(require("./spec.js"), exports);
var versioning_ts_1 = require("./versioning.js");
Object.defineProperty(exports, "WRN_SYNTAX_VERSION", { enumerable: true, get: function () { return versioning_ts_1.WRN_SYNTAX_VERSION; } });
Object.defineProperty(exports, "WRN_SYNTAX_FEATURES", { enumerable: true, get: function () { return versioning_ts_1.WRN_SYNTAX_FEATURES; } });
Object.defineProperty(exports, "createSourceRange", { enumerable: true, get: function () { return versioning_ts_1.createSourceRange; } });
Object.defineProperty(exports, "sliceSource", { enumerable: true, get: function () { return versioning_ts_1.sliceSource; } });
Object.defineProperty(exports, "diagnosticSummary", { enumerable: true, get: function () { return versioning_ts_1.diagnosticSummary; } });
Object.defineProperty(exports, "supportsSyntaxFeature", { enumerable: true, get: function () { return versioning_ts_1.supportsSyntaxFeature; } });
},
"packages/syntax/src/parser.ts": function (module, exports, require, __filename, __dirname) {
@@ -2475,7 +2631,11 @@ function parse(source) {
else {
expect("eq");
}
states.push({ name: sName, valueType, expr: lx.readToLineEnd() });
// State values may be multiline structured expressions. Use the same
// balanced initializer reader as props so formatted arrays/objects
// remain one declaration instead of exposing their inner braces as
// component members on the next line.
states.push({ name: sName, valueType, expr: lx.readPropInitializer() });
break;
}
case "computed": {
@@ -2755,14 +2915,29 @@ function parseHtmlView(src, pos) {
while (i < src.length && isWs(src[i]))
i++;
};
/** Read a `{...}` interpolation (brace-balanced), braces included. */
/** Read a `{...}` interpolation (brace-balanced and quote-aware), braces included. */
const readInterpolation = () => {
const start = i;
let depth = 0;
let quote = null;
for (; i < src.length; i++) {
if (src[i] === "{")
const char = src[i];
if (quote) {
if (char === "\\" && i + 1 < src.length) {
i++;
continue;
}
if (char === quote)
quote = null;
continue;
}
if (char === '"' || char === "'" || char === "`") {
quote = char;
continue;
}
if (char === "{")
depth++;
else if (src[i] === "}" && --depth === 0) {
else if (char === "}" && --depth === 0) {
i++;
return src.slice(start, i);
}
@@ -2852,7 +3027,12 @@ function parseHtmlView(src, pos) {
if (src[i] === "=") {
i++;
skipWs();
attrs.push({ name, value: readQuoted(), event: false });
const value = src[i] === '"' || src[i] === "'"
? readQuoted()
: src[i] === "{"
? readInterpolation()
: fail(`Expected a quoted value or {...} expression after '${name}='`);
attrs.push({ name, value, event: false });
}
else {
attrs.push({ name, value: "", event: false, boolean: true });
@@ -3447,6 +3627,54 @@ function eraseFunctionTypes(source) {
});
}
},
"packages/syntax/src/versioning.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WRN_SYNTAX_FEATURES = exports.WRN_SYNTAX_VERSION = void 0;
exports.createSourceRange = createSourceRange;
exports.sliceSource = sliceSource;
exports.diagnosticSummary = diagnosticSummary;
exports.supportsSyntaxFeature = supportsSyntaxFeature;
/** Current stable syntax contract. Bump only when parsers/codegen need migration. */
exports.WRN_SYNTAX_VERSION = "0.4";
exports.WRN_SYNTAX_FEATURES = Object.freeze({
"typed-declarations": true,
layouts: true,
"server-client-blocks": true,
effects: true,
watch: true,
lifecycle: true,
"embedded-api": true,
realtime: true,
"runtime-markers": true,
});
function createSourceRange(start, end) {
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start) {
throw new RangeError(`Invalid source range ${start}..${end}`);
}
return { start, end };
}
function sliceSource(source, range) {
return source.slice(range.start, range.end);
}
function diagnosticSummary(diagnostics) {
const summary = { errors: 0, warnings: 0, info: 0, codes: {} };
for (const diagnostic of diagnostics) {
if (diagnostic.severity === "error")
summary.errors++;
else if (diagnostic.severity === "warning")
summary.warnings++;
else
summary.info++;
summary.codes[diagnostic.code] = (summary.codes[diagnostic.code] ?? 0) + 1;
}
return summary;
}
function supportsSyntaxFeature(feature) {
return Object.prototype.hasOwnProperty.call(exports.WRN_SYNTAX_FEATURES, feature);
}
}
};
const __aliases = {
+11 -7
View File
@@ -1155,18 +1155,21 @@ function validateRequiredView(document, source, rootKind, rootMatch) {
];
}
function validateLayoutUsage(document, source, rootKind) {
function validateLayoutUsage(document, source, rootKind, rootMatch) {
const diagnostics = [];
if (rootKind !== "page" && /^\s*layout\s*=/m.test(source)) {
const match = /^\s*layout\s*=/m.exec(source);
if (rootKind !== "page") {
const bodyRange = getRootBodyRange(source, rootMatch);
const layoutMember = findRootMembers(source, bodyRange.start, bodyRange.end).find(
(member) => member.name === "layout",
);
if (match) {
if (layoutMember) {
diagnostics.push(
createDiagnostic(
document,
match.index,
match.index + match[0].length,
layoutMember.start,
layoutMember.end,
'`layout = "..."` is only valid inside a page.',
vscode.DiagnosticSeverity.Error,
"wrn-invalid-layout-member",
@@ -1229,7 +1232,7 @@ function validateDocument(document) {
diagnostics.push(...validateRequiredView(document, source, declaration.kind, declaration.match));
diagnostics.push(...validateLayoutUsage(document, source, declaration.kind));
diagnostics.push(...validateLayoutUsage(document, source, declaration.kind, declaration.match));
return diagnostics;
}
@@ -1325,6 +1328,7 @@ module.exports = {
validateDocument,
validateHtmlTags,
validateLifecycleBlocks,
validateLayoutUsage,
validateRootMembers,
validateWatchBlocks,
};
+113 -7
View File
@@ -153,18 +153,95 @@ function findOpeningTagEnd(value) {
function parseAttributes(value) {
const attributes = [];
let index = 0;
const pattern = /[^\s"'=<>`]+(?:\s*=\s*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s"'=<>`]+))?/g;
while (index < value.length) {
while (index < value.length && /\s/.test(value[index])) index += 1;
if (index >= value.length) break;
let match;
const start = index;
while (index < value.length && !/[\s=]/.test(value[index])) index += 1;
while (index < value.length && /\s/.test(value[index])) index += 1;
while ((match = pattern.exec(value)) !== null) {
attributes.push(match[0].trim());
if (value[index] === "=") {
index += 1;
while (index < value.length && /\s/.test(value[index])) index += 1;
const quote = value[index];
if (quote === '"' || quote === "'") {
index += 1;
let escaped = false;
while (index < value.length) {
const character = value[index++];
if (escaped) escaped = false;
else if (character === "\\") escaped = true;
else if (character === quote) break;
}
} else if (value[index] === "{") {
let depth = 0;
let expressionQuote = null;
let escaped = false;
while (index < value.length) {
const character = value[index++];
if (expressionQuote !== null) {
if (escaped) escaped = false;
else if (character === "\\") escaped = true;
else if (character === expressionQuote) expressionQuote = null;
continue;
}
if (character === '"' || character === "'" || character === "`") {
expressionQuote = character;
} else if (character === "{") {
depth += 1;
} else if (character === "}" && --depth === 0) {
break;
}
}
} else {
while (index < value.length && !/\s/.test(value[index])) index += 1;
}
}
const attribute = value.slice(start, index).trim();
if (attribute) attributes.push(attribute);
}
return attributes;
}
function parseStructuredAttribute(attribute) {
const match = /^([^\s=]+)\s*=\s*\{([\s\S]*)\}$/.exec(attribute);
if (!match) return null;
const expression = match[2].trim();
if (!expression.startsWith("[") && !expression.startsWith("{")) return null;
try {
return {
name: match[1],
value: JSON.parse(expression),
};
} catch {
return null;
}
}
function formatAttribute(attribute, indentation, unit) {
const structured = parseStructuredAttribute(attribute);
if (!structured) return [`${indentation}${attribute}`];
const jsonLines = JSON.stringify(structured.value, null, unit).split("\n");
if (jsonLines.length === 1) {
return [`${indentation}${structured.name}={${jsonLines[0]}}`];
}
return [
`${indentation}${structured.name}={${jsonLines[0]}`,
...jsonLines.slice(1, -1).map((line) => `${indentation}${line}`),
`${indentation}${jsonLines.at(-1)}}`,
];
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
@@ -257,7 +334,7 @@ function formatOpeningTag(value, unit, depth, printWidth = 100, multilineAttribu
const lines = [
`${baseIndent}<${parsed.tagName}`,
...parsed.attributes.map((attribute) => `${childIndent}${attribute}`),
...parsed.attributes.flatMap((attribute) => formatAttribute(attribute, childIndent, unit)),
];
if (parsed.selfClosing) {
@@ -328,7 +405,7 @@ function countLeadingClosingBraces(value) {
index += 1;
}
if (value[index] !== "}") {
if (value[index] !== "}" && value[index] !== "]") {
break;
}
@@ -399,6 +476,10 @@ function countStructuralBraces(value) {
openings += 1;
} else if (character === "}") {
closings += 1;
} else if (character === "[") {
openings += 1;
} else if (character === "]") {
closings += 1;
}
}
@@ -457,6 +538,27 @@ function expandInlineControlBlocks(lines) {
});
}
function expandStructuredStateDeclarations(lines, unit) {
return lines.flatMap((line) => {
const match = /^(\s*state\s+[A-Za-z_$][\w$]*\s*=\s*)([\\[{][\s\S]*)$/.exec(line);
if (!match) return [line];
try {
const parsed = JSON.parse(match[2].trim());
const jsonLines = JSON.stringify(parsed, null, unit).split("\n");
if (jsonLines.length === 1) return [`${match[1]}${jsonLines[0]}`];
const leading = match[1].match(/^\s*/)?.[0] ?? "";
return [
`${match[1]}${jsonLines[0]}`,
...jsonLines.slice(1).map((jsonLine) => `${leading}${jsonLine}`),
];
} catch {
return [line];
}
});
}
function formatWrn(source, options = {}) {
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4);
@@ -469,7 +571,10 @@ function formatWrn(source, options = {}) {
let controlDepth = 0;
let index = 0;
const inputLines = expandInlineControlBlocks(source.replace(/\r\n/g, "\n").split("\n"));
const sourceLines = source.replace(/\r\n/g, "\n").split("\n");
const inputLines = expandInlineControlBlocks(
expandStructuredStateDeclarations(sourceLines, unit),
);
const output = [];
@@ -587,6 +692,7 @@ function formatWrn(source, options = {}) {
module.exports = {
countStructuralBraces,
formatOpeningTag,
formatAttribute,
formatWrn,
parseAttributes,
parseOpeningTag,
+36
View File
@@ -31,6 +31,7 @@ const {
maskLeadingTrivia,
validateBalancedCharacters,
validateHtmlTags,
validateLayoutUsage,
validateRootMembers,
} = require("../src/diagnostics");
Module._load = originalLoad;
@@ -136,6 +137,41 @@ test("ignores member-like words inside component line and block comments", () =>
);
});
test("allows a component prop named layout but rejects a root layout member", () => {
const component = `component Card {
props {
layout = "vertical"
}
view { <article>{layout}</article> }
}`;
const componentDeclaration = findTopLevelDeclaration({}, component);
assert.deepEqual(
validateLayoutUsage(
mockDocument(component),
component,
componentDeclaration.kind,
componentDeclaration.match,
),
[],
);
const invalid = `component Card {
layout = "dashboard"
view { <article></article> }
}`;
const invalidDeclaration = findTopLevelDeclaration({}, invalid);
const diagnostics = validateLayoutUsage(
mockDocument(invalid),
invalid,
invalidDeclaration.kind,
invalidDeclaration.match,
);
assert.equal(diagnostics.length, 1);
assert.equal(diagnostics[0].code, "wrn-invalid-layout-member");
});
test("ignores HTML-like tags inside WRN comments", () => {
const source = `// The <section> below listens for child events.
// <ComboBox> is only documentation in this comment.
+63
View File
@@ -20,6 +20,69 @@ test("preserves nested prop defaults while formatting", () => {
assert.equal(formatWrn(formatted, { insertSpaces: true, tabSize: 2 }), formatted);
});
test("formats native JSON state values with readable indentation", () => {
const source = `component Footer {
state footerItems = [{"label":"Accessibility","href":"/accessibility","value":"accessibility"},{"label":"Privacy","href":"/privacy","value":"privacy"}]
view { <footer></footer> }
}
`;
const expected = `component Footer {
state footerItems = [
{
"label": "Accessibility",
"href": "/accessibility",
"value": "accessibility"
},
{
"label": "Privacy",
"href": "/privacy",
"value": "privacy"
}
]
view { <footer></footer> }
}
`;
const options = { insertSpaces: true, tabSize: 2 };
assert.equal(formatWrn(source, options), expected);
assert.equal(formatWrn(expected, options), expected);
});
test("formats direct structured component props and preserves expression props", () => {
const source = `component Demo {
view {
<Footer items={footerItems} options={{"dense":true,"theme":"public"}} links={[{"label":"Privacy","href":"/privacy"},{"label":"Contact","href":"/contact"}]} />
}
}
`;
const expected = `component Demo {
view {
<Footer
items={footerItems}
options={{
"dense": true,
"theme": "public"
}}
links={[
{
"label": "Privacy",
"href": "/privacy"
},
{
"label": "Contact",
"href": "/contact"
}
]}
/>
}
}
`;
const options = { insertSpaces: true, tabSize: 2, multilineAttributes: true };
assert.equal(formatWrn(source, options), expected);
assert.equal(formatWrn(expected, options), expected);
});
test("formats public event declarations as individual props members", () => {
const source = `component Search {\nprops { value = "" @event search = function @event clear = function }\nview { <input/> }\n}\n`;
const formatted = formatWrn(source, { insertSpaces: true, tabSize: 2 });
+19
View File
@@ -102,6 +102,25 @@ try {
compiler.compileWireFile(good);
ok("compiler accepts valid .wrn");
const structuredProps = `page FooterExample {
state footerItems = [
{
"label": "Accessibility",
"href": "/accessibility",
"value": "accessibility"
}
]
view {
<Footer
items={footerItems}
options={{"dense":true}}
links={[{"label":"Privacy","href":"/privacy"}]}
/>
}
}`;
compiler.compileWireFile(structuredProps);
ok("compiler accepts native structured state and unquoted prop expressions");
const badSrc = `page Home {\n view { <h1>Hi</h2> }\n}`;
let threw = false;
try {