diff --git a/.publish/ai/package.json b/.publish/ai/package.json index 40dcecdd..5300f89e 100644 --- a/.publish/ai/package.json +++ b/.publish/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "license": "MIT", diff --git a/.publish/authz/package.json b/.publish/authz/package.json index be53332c..fc149291 100644 --- a/.publish/authz/package.json +++ b/.publish/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/authz — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/cli/package.json b/.publish/cli/package.json index 207467bf..7046bdf7 100644 --- a/.publish/cli/package.json +++ b/.publish/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/cli — part of the WrNexus framework.", "license": "MIT", @@ -28,16 +28,16 @@ "wrnexus": "./dist/index.js" }, "dependencies": { - "@wrnexus/core": "^0.2.36", - "@wrnexus/router": "^0.2.36", - "@wrnexus/csr": "^0.2.36", - "@wrnexus/compiler": "^0.2.36", - "@wrnexus/styles": "^0.2.36", - "@wrnexus/dev-server": "^0.2.36", - "@wrnexus/ui": "^0.2.36", - "@wrnexus/validation": "^0.2.36", - "@wrnexus/i18n": "^0.2.36", - "@wrnexus/db": "^0.2.36" + "@wrnexus/core": "^0.2.37", + "@wrnexus/router": "^0.2.37", + "@wrnexus/csr": "^0.2.37", + "@wrnexus/compiler": "^0.2.37", + "@wrnexus/styles": "^0.2.37", + "@wrnexus/dev-server": "^0.2.37", + "@wrnexus/ui": "^0.2.37", + "@wrnexus/validation": "^0.2.37", + "@wrnexus/i18n": "^0.2.37", + "@wrnexus/db": "^0.2.37" }, "files": [ "dist" diff --git a/.publish/compiler/package.json b/.publish/compiler/package.json index d9c6ded5..05719e81 100644 --- a/.publish/compiler/package.json +++ b/.publish/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/compiler — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/core/package.json b/.publish/core/package.json index a8c2bcb4..824e6058 100644 --- a/.publish/core/package.json +++ b/.publish/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/core — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/csr/package.json b/.publish/csr/package.json index b147d693..7c45b7dd 100644 --- a/.publish/csr/package.json +++ b/.publish/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/csr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.36" + "@wrnexus/core": "^0.2.37" }, "files": [ "dist" diff --git a/.publish/db/package.json b/.publish/db/package.json index 83791d8c..1bab2093 100644 --- a/.publish/db/package.json +++ b/.publish/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/db — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/dev-server/package.json b/.publish/dev-server/package.json index a06bfe7f..3ef44ee7 100644 --- a/.publish/dev-server/package.json +++ b/.publish/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/dev-server — part of the WrNexus framework.", "license": "MIT", @@ -25,18 +25,18 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.36", - "@wrnexus/router": "^0.2.36", - "@wrnexus/ssr": "^0.2.36", - "@wrnexus/csr": "^0.2.36", - "@wrnexus/compiler": "^0.2.36", - "@wrnexus/styles": "^0.2.36", - "@wrnexus/ui": "^0.2.36", - "@wrnexus/validation": "^0.2.36", - "@wrnexus/i18n": "^0.2.36", - "@wrnexus/db": "^0.2.36", - "@wrnexus/pubsub": "^0.2.36", - "@wrnexus/uploader": "^0.2.36" + "@wrnexus/core": "^0.2.37", + "@wrnexus/router": "^0.2.37", + "@wrnexus/ssr": "^0.2.37", + "@wrnexus/csr": "^0.2.37", + "@wrnexus/compiler": "^0.2.37", + "@wrnexus/styles": "^0.2.37", + "@wrnexus/ui": "^0.2.37", + "@wrnexus/validation": "^0.2.37", + "@wrnexus/i18n": "^0.2.37", + "@wrnexus/db": "^0.2.37", + "@wrnexus/pubsub": "^0.2.37", + "@wrnexus/uploader": "^0.2.37" }, "files": [ "dist" diff --git a/.publish/encryption/package.json b/.publish/encryption/package.json index 56cd5b4e..376fc846 100644 --- a/.publish/encryption/package.json +++ b/.publish/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/encryption — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/helpers/package.json b/.publish/helpers/package.json index 901f0b55..26c57a8b 100644 --- a/.publish/helpers/package.json +++ b/.publish/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.36" + "@wrnexus/core": "^0.2.37" }, "files": [ "dist" diff --git a/.publish/i18n/package.json b/.publish/i18n/package.json index 8d11e303..e723badb 100644 --- a/.publish/i18n/package.json +++ b/.publish/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/i18n — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.36" + "@wrnexus/core": "^0.2.37" }, "files": [ "dist" diff --git a/.publish/jwt/package.json b/.publish/jwt/package.json index f2f668f2..e649e195 100644 --- a/.publish/jwt/package.json +++ b/.publish/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/jwt — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/mobile/package.json b/.publish/mobile/package.json index 4f6792fe..a5fb6bda 100644 --- a/.publish/mobile/package.json +++ b/.publish/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/mobile — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/native": "^0.2.36" + "@wrnexus/native": "^0.2.37" }, "files": [ "dist" diff --git a/.publish/native/package.json b/.publish/native/package.json index 575bb9c5..7e16e6e0 100644 --- a/.publish/native/package.json +++ b/.publish/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/native — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/oauth/package.json b/.publish/oauth/package.json index 83be7b70..4be3a23f 100644 --- a/.publish/oauth/package.json +++ b/.publish/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/oauth — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/pubsub/package.json b/.publish/pubsub/package.json index c83faceb..15616940 100644 --- a/.publish/pubsub/package.json +++ b/.publish/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/pubsub — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/queue/package.json b/.publish/queue/package.json index c7c5c3dc..120f2e9f 100644 --- a/.publish/queue/package.json +++ b/.publish/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/queue — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/reactive/package.json b/.publish/reactive/package.json index b67be12e..8ce044a0 100644 --- a/.publish/reactive/package.json +++ b/.publish/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/reactive — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/router/package.json b/.publish/router/package.json index 678917bc..20c02fd4 100644 --- a/.publish/router/package.json +++ b/.publish/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/router — part of the WrNexus framework.", "license": "MIT", @@ -21,8 +21,8 @@ } }, "dependencies": { - "@wrnexus/compiler": "^0.2.36", - "@wrnexus/core": "^0.2.36" + "@wrnexus/compiler": "^0.2.37", + "@wrnexus/core": "^0.2.37" }, "files": [ "dist" diff --git a/.publish/ssr/package.json b/.publish/ssr/package.json index a940e943..0c9cf02e 100644 --- a/.publish/ssr/package.json +++ b/.publish/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/ssr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.36" + "@wrnexus/core": "^0.2.37" }, "files": [ "dist" diff --git a/.publish/styles/package.json b/.publish/styles/package.json index 26fee7f0..923aa85f 100644 --- a/.publish/styles/package.json +++ b/.publish/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/styles — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/uploader": "^0.2.36" + "@wrnexus/uploader": "^0.2.37" }, "files": [ "dist" diff --git a/.publish/test/package.json b/.publish/test/package.json index eca794c3..34f9a4b5 100644 --- a/.publish/test/package.json +++ b/.publish/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/test — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/tracking/package.json b/.publish/tracking/package.json index 8b228f8e..0f2f5a6d 100644 --- a/.publish/tracking/package.json +++ b/.publish/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/tracking — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/ui/package.json b/.publish/ui/package.json index fef106cf..aa89da4e 100644 --- a/.publish/ui/package.json +++ b/.publish/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/ui — part of the WrNexus framework.", "license": "MIT", @@ -22,7 +22,7 @@ "./ui.css": "./ui.css" }, "dependencies": { - "@wrnexus/core": "^0.2.36" + "@wrnexus/core": "^0.2.37" }, "files": [ "dist", diff --git a/.publish/uploader/package.json b/.publish/uploader/package.json index ac5b8831..502f325a 100644 --- a/.publish/uploader/package.json +++ b/.publish/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/uploader — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.36" + "@wrnexus/core": "^0.2.37" }, "files": [ "dist" diff --git a/.publish/validation/package.json b/.publish/validation/package.json index 2300b04a..5d223b9c 100644 --- a/.publish/validation/package.json +++ b/.publish/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "description": "@wrnexus/validation — part of the WrNexus framework.", "license": "MIT", diff --git a/.vscode/settings.json b/.vscode/settings.json index 72bfdd76..b5b7edb5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -42,5 +42,21 @@ "editors/vscode/src/compiler.cjs": true }, - "wrnexus.diagnostics.enable": true + "wrnexus.diagnostics.enable": true, + + "editor.semanticHighlighting.enabled": true, + "editor.semanticTokenColorCustomizations": { + "rules": { + "variable:wrn": { + "foreground": "#8B5CF6" + }, + "variable.declaration:wrn": { + "foreground": "#A78BFA", + "fontStyle": "bold" + }, + "variable.modification:wrn": { + "foreground": "#F59E0B" + } + } + } } diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 1b9d47fe..d74bd636 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -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.7", + "version": "0.2.9", "publisher": "wrnexus", "private": true, "license": "SEE LICENSE IN LICENSE", @@ -225,7 +225,30 @@ } ] } - } + }, + "semanticTokenScopes": [ + { + "language": "wrn", + "scopes": { + "variable.declaration": [ + "variable.other.readwrite.wrn", + "variable.other.readwrite.declaration.wrn" + ], + "variable": [ + "variable.other.readwrite.wrn" + ], + "parameter": [ + "variable.parameter.wrn" + ], + "function": [ + "entity.name.function.wrn" + ], + "property": [ + "variable.other.property.wrn" + ] + } + } + ] }, "scripts": { "build:compiler": "bun build ../../packages/compiler/src/index.ts --target=node --format=cjs --outfile=src/compiler.cjs", diff --git a/editors/vscode/src/compiler.cjs b/editors/vscode/src/compiler.cjs index ff6acee3..c6ee2f41 100644 --- a/editors/vscode/src/compiler.cjs +++ b/editors/vscode/src/compiler.cjs @@ -546,14 +546,22 @@ function parseHtmlView(src, pos) { return src.slice(start, i); }; const readAttributeName = () => { - const first = src[i]; - if (i >= src.length || !isNameStart(first) && first !== ":" && first !== "$") { + if (i >= src.length) { return fail("Expected an attribute name"); } - const start = i++; - while (i < src.length && isAttributeNamePart(src[i], src[i + 1])) { + const start = i; + while (i < src.length) { + const char = src[i]; + const next = src[i + 1]; + if (char === "=" || char === ">" || char === '"' || char === "'" || char === " " || char === "\t" || char === ` +` || char === "\r" || char === "/" && next === ">") { + break; + } i++; } + if (i === start) { + return fail("Expected an attribute name"); + } return src.slice(start, i); }; const parseTag = () => { @@ -1340,7 +1348,8 @@ function behaviorAttribute(behavior) { if (!behavior) { return ""; } - return ` data-wrn-behavior="${attrEscape(JSON.stringify(behavior))}"`; + const encoded = Buffer.from(JSON.stringify(behavior), "utf8").toString("base64"); + return ` data-wrn-behavior="${encoded}"`; } var INTERP_RE = /\{([^{}]+)\}/g; function exprRefsState(expr, stateNames) { @@ -1435,14 +1444,17 @@ function renderComponentNode(node, ctx) { const initialConditionalClasses = conditionalClasses.map(({ className, expression }) => { return `\${(${ctx.resolveExpr(expression)}) ? ${JSON.stringify(` ${className}`)} : ""}`; }).join(""); - const classAttribute = staticClasses.length > 0 || conditionalClasses.length > 0 ? ` class="${compileAttrValue(staticClasses.join(" "), ctx)}${initialConditionalClasses}"` : ""; + const staticClassValue = staticClasses.join(" "); + const classHasReactiveExpression = staticClassValue.includes("{") && exprRefsState(staticClassValue, ctx.stateNames); + const classAttribute = staticClasses.length > 0 || conditionalClasses.length > 0 ? ` class="${compileAttrValue(staticClassValue, ctx)}${initialConditionalClasses}"` : ""; + const classReactiveBinding = classHasReactiveExpression ? ` data-wrn-bind-class="${escLit(attrEscape(JSON.stringify(["class", staticClassValue])))}"` : ""; const classBindings = conditionalClasses.map(({ className, expression }, index) => { const marker = attrEscape(JSON.stringify([className, expression])); return ` data-wrn-class-${index}="${escLit(marker)}"`; }).join(""); const loops = loopVarsOf(node); const childCtx = loops.length > 0 ? { ...ctx, loopVars: new Set([...ctx.loopVars ?? [], ...loops]) } : ctx; - const allAttrs = `${classAttribute}${classBindings}${attrs}`; + const allAttrs = `${classAttribute}` + `${classReactiveBinding}` + `${classBindings}` + `${attrs}`; if (VOID_ELEMENTS.has(node.tag.toLowerCase())) { return `<${node.tag}${allAttrs}>`; } diff --git a/editors/vscode/src/extension.js b/editors/vscode/src/extension.js index 10442c1d..c5d9ab7b 100644 --- a/editors/vscode/src/extension.js +++ b/editors/vscode/src/extension.js @@ -271,10 +271,227 @@ function activate(context) { registerCompletionProvider(context); registerDefinitionProvider(context); registerFormatter(context); + + context.subscriptions.push( + vscode.languages.registerDocumentSemanticTokensProvider( + { + language: "wrn", + }, + wrnSemanticTokensProvider, + semanticTokenLegend, + ), + ); } function deactivate() {} +const semanticTokenLegend = new vscode.SemanticTokensLegend( + ["variable"], + ["declaration", "modification"], +); + +/** + * @param {string} value + * @returns {string} + */ +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +/** + * @param {string} text + * @returns {Map>} + */ +function collectStateVariables(text) { + const states = new Map(); + const statePattern = /\bstate\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?==|;|\r?$)/gm; + + let match; + + while ((match = statePattern.exec(text)) !== null) { + const name = match[1]; + + if (!name) { + continue; + } + + const offset = match.index + match[0].lastIndexOf(name); + const declarations = states.get(name) || new Set(); + + declarations.add(offset); + states.set(name, declarations); + } + + return states; +} + +/** + * Comments are excluded, but quoted attribute expressions are deliberately + * included because WRN state references commonly appear in values such as + * class:flex="centered" and @click="enabled = !enabled". + * + * @param {string} text + * @returns {Array<{start: number, end: number}>} + */ +function collectCommentRanges(text) { + const ranges = []; + let index = 0; + let quote = null; + let escaped = false; + + while (index < text.length) { + const current = text[index]; + const next = text[index + 1]; + + if (quote) { + if (escaped) { + escaped = false; + index++; + continue; + } + + if (current === "\\") { + escaped = true; + index++; + continue; + } + + if (current === quote) { + quote = null; + } + + index++; + continue; + } + + if (current === '"' || current === "'" || current === "`") { + quote = current; + index++; + continue; + } + + if (current === "/" && next === "/") { + const start = index; + index += 2; + + while (index < text.length && text[index] !== "\n") { + index++; + } + + ranges.push({ start, end: index }); + continue; + } + + if (current === "/" && next === "*") { + const start = index; + index += 2; + + while (index < text.length && !(text[index] === "*" && text[index + 1] === "/")) { + index++; + } + + index = Math.min(text.length, index + 2); + ranges.push({ start, end: index }); + continue; + } + + index++; + } + + return ranges; +} + +/** + * @param {number} offset + * @param {Array<{start: number, end: number}>} ranges + * @returns {boolean} + */ +function isInsideComment(offset, ranges) { + let low = 0; + let high = ranges.length - 1; + + while (low <= high) { + const middle = Math.floor((low + high) / 2); + const range = ranges[middle]; + + if (!range) { + return false; + } + + if (offset < range.start) { + high = middle - 1; + } else if (offset >= range.end) { + low = middle + 1; + } else { + return true; + } + } + + return false; +} + +/** + * @param {string} text + * @param {number} offset + * @param {number} length + * @returns {boolean} + */ +function isStateModification(text, offset, length) { + const after = text.slice(offset + length).match(/^\s*(=|\+=|-=|\*=|\/=|%=|\+\+|--)/); + + if (after) { + return true; + } + + const before = text.slice(Math.max(0, offset - 8), offset).match(/(\+\+|--)\s*$/); + + return Boolean(before); +} + +const wrnSemanticTokensProvider = { + /** + * @param {vscode.TextDocument} document + * @param {vscode.CancellationToken} token + */ + provideDocumentSemanticTokens(document, token) { + const builder = new vscode.SemanticTokensBuilder(semanticTokenLegend); + const text = document.getText(); + const states = collectStateVariables(text); + const commentRanges = collectCommentRanges(text); + + for (const [name, declarationOffsets] of states) { + if (token.isCancellationRequested) { + return builder.build(); + } + + const pattern = new RegExp(`(? export const websocket = { evt(ws, ...args) { b } } */ +import { Buffer } from "node:buffer"; + import { VOID_ELEMENTS, type Attr, type DataMode, type PageAst, type ViewNode } from "./parser.ts"; interface RenderBinding { @@ -804,6 +806,8 @@ interface CompCtx { /** `data-for` loop variables in scope — their mustaches stay literal for the * client's list renderer (never baked server-side, since they have no value). */ loopVars?: Set; + /** Local identifiers introduced by server-rendered `{#each}` blocks. */ + serverLocals?: Set; } interface ComponentBehavior { @@ -940,9 +944,19 @@ function exprRefsState(expr: string, stateNames: Set): boolean { } function viewHasEvents(nodes: ViewNode[]): boolean { - return nodes.some( - (n) => n.type === "element" && (n.attrs.some((a) => a.event) || viewHasEvents(n.children)), - ); + return nodes.some((node) => { + if (node.type === "text") return false; + + if (node.type === "each") { + return viewHasEvents(node.body) || viewHasEvents(node.empty); + } + + if (node.type === "if") { + return node.branches.some((branch) => viewHasEvents(branch.body)); + } + + return node.attrs.some((attr) => attr.event) || viewHasEvents(node.children); + }); } /** @@ -1004,14 +1018,65 @@ function compileAttrValue(raw: string, ctx: CompCtx): string { return out + escLit(attrEscape(raw.slice(last))); } +function renderComponentIfNode(node: IfNode, ctx: CompCtx): string { + let expression = "``"; + + for (let index = node.branches.length - 1; index >= 0; index--) { + const branch = node.branches[index]!; + const body = branch.body.map((child) => renderComponentNode(child, ctx)).join(""); + const bodyExpression = "`" + body + "`"; + + expression = + branch.cond === null + ? bodyExpression + : `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`; + } + + return "${" + expression + "}"; +} + +function renderComponentEachNode(node: EachNode, ctx: CompCtx): string { + const item = node.item; + const index = node.index ?? "__wi"; + const list = ctx.resolveExpr(node.list); + + const childCtx: CompCtx = { + ...ctx, + serverLocals: new Set([...(ctx.serverLocals ?? []), item, index]), + }; + + const body = node.body.map((child) => renderComponentNode(child, childCtx)).join(""); + const empty = node.empty.map((child) => renderComponentNode(child, ctx)).join(""); + + return ( + "${(() => { const __wl = Array.isArray(" + + list + + ") ? (" + + list + + ") : []; return __wl.length ? __wl.map((" + + item + + ", " + + index + + ") => `" + + body + + '`).join("") : `' + + empty + + "`; })()}" + ); +} + /** Render a component view node into template-literal-ready source. */ function renderComponentNode(node: ViewNode, ctx: CompCtx): string { if (node.type === "text") return compileText(node.value, ctx); - if (node.type === "each" || node.type === "if") { - throw new Error( - "Server `{#each}` / `{#if}` blocks are supported in pages, not components. Move them into a page (or use data-for / data-show on the client).", - ); + + if (node.type === "each") { + return renderComponentEachNode(node, ctx); } + + if (node.type === "if") { + return renderComponentIfNode(node, ctx); + } + if (isComponentTag(node.tag)) { return renderNestedComponentInvocation(node, ctx); } @@ -1080,6 +1145,9 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string { : ""; const classBindings = conditionalClasses + .filter(({ expression }) => { + return !ctx.serverLocals || !exprRefsState(expression, ctx.serverLocals); + }) .map(({ className, expression }, index) => { const marker = attrEscape(JSON.stringify([className, expression])); @@ -1200,6 +1268,9 @@ function __wireAttr(v: any): string { return String(v == null ? "" : v).replace(/[&<>"]/g, (c) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """, ); +} +function __wireRaw(v: any): string { + return String(v == null ? "" : v); }`); if (needsScope) { diff --git a/packages/compiler/src/parser.ts b/packages/compiler/src/parser.ts index e5d28398..0fda6b93 100644 --- a/packages/compiler/src/parser.ts +++ b/packages/compiler/src/parser.ts @@ -469,13 +469,6 @@ export function parseHtmlView(src: string, pos: number): { nodes: ViewNode[]; en const isTagNamePart = (c: string): boolean => /[A-Za-z0-9_$:.-]/.test(c); - const isAttributeNamePart = (c: string, next: string | undefined): boolean => { - if (c === "/") { - return next !== ">"; - } - - return /[A-Za-z0-9_$:.[\]%-]/.test(c); - }; const isWs = (c: string): boolean => c === " " || c === "\t" || c === "\n" || c === "\r"; const fail = (msg: string): never => { diff --git a/packages/compiler/test/compiler.test.ts b/packages/compiler/test/compiler.test.ts index 5536929f..221c3a03 100644 --- a/packages/compiler/test/compiler.test.ts +++ b/packages/compiler/test/compiler.test.ts @@ -689,3 +689,51 @@ component PageHeader { expect(output).toContain("dark:bg-white/10"); }); +test("components support each blocks", () => { + const output = generate( + parse(` +component PageHeader { + props { + highlights = [] + } + + view { +
+ {#each highlights as highlight} + {highlight.title} + {/each} +
+ } +} +`), + ); + + expect(output).toContain("highlights"); + + expect(output).toContain("highlight.title"); + + expect(output).not.toContain("supported in pages, not components"); +}); +test("components support if blocks", () => { + const output = generate( + parse(` +component Banner { + props { + visible = true + } + + view { + {#if visible} +
Visible
+ {:else} +
Hidden
+ {/if} + } +} +`), + ); + + expect(output).toContain("visible"); + expect(output).toContain("Visible"); + expect(output).toContain("Hidden"); +}); diff --git a/packages/core/package.json b/packages/core/package.json index 801732ec..a993bde5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/csr/package.json b/packages/csr/package.json index e2d98715..53c1835d 100644 --- a/packages/csr/package.json +++ b/packages/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/db/package.json b/packages/db/package.json index c0955a9a..c4341a9c 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.2.36", + "version": "0.2.37", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json index 5743656a..616c57ee 100644 --- a/packages/dev-server/package.json +++ b/packages/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/encryption/package.json b/packages/encryption/package.json index 5210c542..8c5f0ac3 100644 --- a/packages/encryption/package.json +++ b/packages/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.2.36", + "version": "0.2.37", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/helpers/package.json b/packages/helpers/package.json index d9e10b91..2fd223e3 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.2.36", + "version": "0.2.37", "private": true, "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 18a4159c..3a9b5346 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.2.36", + "version": "0.2.37", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/jwt/package.json b/packages/jwt/package.json index 60b90467..cecb220d 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.2.36", + "version": "0.2.37", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 0bdea454..33a0ee95 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/native/package.json b/packages/native/package.json index bd273ccb..2041d009 100644 --- a/packages/native/package.json +++ b/packages/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/oauth/package.json b/packages/oauth/package.json index a3bae2cb..819fc28c 100644 --- a/packages/oauth/package.json +++ b/packages/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.2.36", + "version": "0.2.37", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/pubsub/package.json b/packages/pubsub/package.json index 202b2182..c09aa0ac 100644 --- a/packages/pubsub/package.json +++ b/packages/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.2.36", + "version": "0.2.37", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/queue/package.json b/packages/queue/package.json index e5b0db00..f30d3ced 100644 --- a/packages/queue/package.json +++ b/packages/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.2.36", + "version": "0.2.37", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/reactive/package.json b/packages/reactive/package.json index 9adadd60..907e59f3 100644 --- a/packages/reactive/package.json +++ b/packages/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/router/package.json b/packages/router/package.json index c1742ab1..e7b5131f 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/ssr/package.json b/packages/ssr/package.json index a29d56fa..5333b4e1 100644 --- a/packages/ssr/package.json +++ b/packages/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/styles/package.json b/packages/styles/package.json index a8b9e0f8..72edddfa 100644 --- a/packages/styles/package.json +++ b/packages/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.2.36", + "version": "0.2.37", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/test/package.json b/packages/test/package.json index eff2149d..43bd6a26 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.2.36", + "version": "0.2.37", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/tracking/package.json b/packages/tracking/package.json index 9647d0bb..fe252a9d 100644 --- a/packages/tracking/package.json +++ b/packages/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.2.36", + "version": "0.2.37", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index e24539ee..27d3c430 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.2.36", + "version": "0.2.37", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/uploader/package.json b/packages/uploader/package.json index a69c5222..79d67ed8 100644 --- a/packages/uploader/package.json +++ b/packages/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.2.36", + "version": "0.2.37", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/validation/package.json b/packages/validation/package.json index c3a3b9c0..d9587f42 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.2.36", + "version": "0.2.37", "private": true, "type": "module", "main": "src/index.ts", diff --git a/update-package-versions.mjs b/update-package-versions.mjs new file mode 100644 index 00000000..5ecbc1e0 --- /dev/null +++ b/update-package-versions.mjs @@ -0,0 +1,63 @@ +import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +const version = "0.2.37"; + +const dependencyGroups = [ + "dependencies", + "devDependencies", + "peerDependencies", + "optionalDependencies", +]; + +for (const entry of readdirSync("packages", { + withFileTypes: true, +})) { + if (!entry.isDirectory()) { + continue; + } + + const packageFile = join("packages", entry.name, "package.json"); + + if (!existsSync(packageFile)) { + continue; + } + + const packageJson = JSON.parse(readFileSync(packageFile, "utf8")); + + packageJson.version = version; + + for (const group of dependencyGroups) { + const dependencies = packageJson[group]; + + if (!dependencies || typeof dependencies !== "object") { + continue; + } + + for (const dependencyName of Object.keys(dependencies)) { + if (!dependencyName.startsWith("@wrnexus/")) { + continue; + } + + const currentVersion = dependencies[dependencyName]; + + if (typeof currentVersion !== "string") { + continue; + } + + if (currentVersion.startsWith("workspace:")) { + continue; + } + + if (currentVersion.startsWith("^")) { + dependencies[dependencyName] = `^${version}`; + } else if (currentVersion.startsWith("~")) { + dependencies[dependencyName] = `~${version}`; + } else { + dependencies[dependencyName] = version; + } + } + } + + writeFileSync(packageFile, `${JSON.stringify(packageJson, null, 2)}\n`); +}