release: WRNexusJS 0.5.10
This commit is contained in:
@@ -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`,
|
||||
|
||||
@@ -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
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user