From e6dbb5b0fcb2499fc5e33f8950aba8aac565f2be Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Tue, 14 Jul 2026 17:34:51 +0530 Subject: [PATCH] release: WRNexusJS 0.2.29 --- .publish/ai/package.json | 2 +- .publish/authz/package.json | 2 +- .publish/cli/package.json | 22 +-- .publish/compiler/package.json | 2 +- .publish/core/package.json | 2 +- .publish/csr/package.json | 4 +- .publish/db/package.json | 2 +- .publish/dev-server/package.json | 26 ++-- .publish/encryption/package.json | 2 +- .publish/helpers/package.json | 4 +- .publish/i18n/package.json | 4 +- .publish/jwt/package.json | 2 +- .publish/mobile/package.json | 4 +- .publish/native/package.json | 2 +- .publish/oauth/package.json | 2 +- .publish/pubsub/package.json | 2 +- .publish/queue/package.json | 2 +- .publish/reactive/package.json | 2 +- .publish/router/package.json | 6 +- .publish/ssr/package.json | 4 +- .publish/styles/package.json | 4 +- .publish/test/package.json | 2 +- .publish/tracking/package.json | 2 +- .publish/ui/package.json | 4 +- .publish/uploader/package.json | 4 +- .publish/validation/package.json | 2 +- editors/vscode/package.json | 2 +- editors/vscode/src/definition.js | 33 +---- editors/vscode/src/diagnostics.js | 215 +++++++-------------------- editors/vscode/wrnexus-0.2.5.vsix | Bin 40634 -> 40722 bytes editors/vscode/wrnexus-0.2.6.vsix | Bin 0 -> 40722 bytes packages/ai/package.json | 2 +- packages/authz/package.json | 2 +- packages/cli/package.json | 2 +- packages/cli/src/update.ts | 10 ++ packages/cli/src/workspace.ts | 230 +++++++++++++++++++++++++++-- packages/compiler/package.json | 2 +- packages/core/package.json | 2 +- packages/csr/package.json | 2 +- packages/db/package.json | 2 +- packages/dev-server/package.json | 2 +- packages/dev-server/src/gateway.ts | 81 +++++++++- packages/encryption/package.json | 2 +- packages/helpers/package.json | 2 +- packages/helpers/src/index.ts | 8 + packages/helpers/src/workspace.ts | 58 ++++++++ packages/i18n/package.json | 2 +- packages/jwt/package.json | 2 +- packages/mobile/package.json | 2 +- packages/native/package.json | 2 +- packages/oauth/package.json | 2 +- packages/pubsub/package.json | 2 +- packages/queue/package.json | 2 +- packages/reactive/package.json | 2 +- packages/router/package.json | 2 +- packages/ssr/package.json | 2 +- packages/styles/package.json | 2 +- packages/test/package.json | 2 +- packages/tracking/package.json | 2 +- packages/ui/package.json | 2 +- packages/uploader/package.json | 2 +- packages/validation/package.json | 2 +- 62 files changed, 513 insertions(+), 292 deletions(-) create mode 100644 editors/vscode/wrnexus-0.2.6.vsix create mode 100644 packages/helpers/src/workspace.ts diff --git a/.publish/ai/package.json b/.publish/ai/package.json index c535f49d..c81bfbc2 100644 --- a/.publish/ai/package.json +++ b/.publish/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.2.28", + "version": "0.2.29", "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 41f51e5d..a067d076 100644 --- a/.publish/authz/package.json +++ b/.publish/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.2.28", + "version": "0.2.29", "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 29cf36cb..4f34180a 100644 --- a/.publish/cli/package.json +++ b/.publish/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.2.28", + "version": "0.2.29", "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.28", - "@wrnexus/router": "^0.2.28", - "@wrnexus/csr": "^0.2.28", - "@wrnexus/compiler": "^0.2.28", - "@wrnexus/styles": "^0.2.28", - "@wrnexus/dev-server": "^0.2.28", - "@wrnexus/ui": "^0.2.28", - "@wrnexus/validation": "^0.2.28", - "@wrnexus/i18n": "^0.2.28", - "@wrnexus/db": "^0.2.28" + "@wrnexus/core": "^0.2.29", + "@wrnexus/router": "^0.2.29", + "@wrnexus/csr": "^0.2.29", + "@wrnexus/compiler": "^0.2.29", + "@wrnexus/styles": "^0.2.29", + "@wrnexus/dev-server": "^0.2.29", + "@wrnexus/ui": "^0.2.29", + "@wrnexus/validation": "^0.2.29", + "@wrnexus/i18n": "^0.2.29", + "@wrnexus/db": "^0.2.29" }, "files": [ "dist" diff --git a/.publish/compiler/package.json b/.publish/compiler/package.json index 00ce5ecb..0baba426 100644 --- a/.publish/compiler/package.json +++ b/.publish/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.2.28", + "version": "0.2.29", "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 4fb07c70..a4a79e4f 100644 --- a/.publish/core/package.json +++ b/.publish/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.2.28", + "version": "0.2.29", "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 f8b4222d..218e7994 100644 --- a/.publish/csr/package.json +++ b/.publish/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.2.28", + "version": "0.2.29", "type": "module", "description": "@wrnexus/csr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.28" + "@wrnexus/core": "^0.2.29" }, "files": [ "dist" diff --git a/.publish/db/package.json b/.publish/db/package.json index 978799ae..75003b05 100644 --- a/.publish/db/package.json +++ b/.publish/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.2.28", + "version": "0.2.29", "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 6b496205..324ed4f5 100644 --- a/.publish/dev-server/package.json +++ b/.publish/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.2.28", + "version": "0.2.29", "type": "module", "description": "@wrnexus/dev-server — part of the WrNexus framework.", "license": "MIT", @@ -25,18 +25,18 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.28", - "@wrnexus/router": "^0.2.28", - "@wrnexus/ssr": "^0.2.28", - "@wrnexus/csr": "^0.2.28", - "@wrnexus/compiler": "^0.2.28", - "@wrnexus/styles": "^0.2.28", - "@wrnexus/ui": "^0.2.28", - "@wrnexus/validation": "^0.2.28", - "@wrnexus/i18n": "^0.2.28", - "@wrnexus/db": "^0.2.28", - "@wrnexus/pubsub": "^0.2.28", - "@wrnexus/uploader": "^0.2.28" + "@wrnexus/core": "^0.2.29", + "@wrnexus/router": "^0.2.29", + "@wrnexus/ssr": "^0.2.29", + "@wrnexus/csr": "^0.2.29", + "@wrnexus/compiler": "^0.2.29", + "@wrnexus/styles": "^0.2.29", + "@wrnexus/ui": "^0.2.29", + "@wrnexus/validation": "^0.2.29", + "@wrnexus/i18n": "^0.2.29", + "@wrnexus/db": "^0.2.29", + "@wrnexus/pubsub": "^0.2.29", + "@wrnexus/uploader": "^0.2.29" }, "files": [ "dist" diff --git a/.publish/encryption/package.json b/.publish/encryption/package.json index d14ccb37..845cd9eb 100644 --- a/.publish/encryption/package.json +++ b/.publish/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.2.28", + "version": "0.2.29", "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 a3d1ca50..1f538247 100644 --- a/.publish/helpers/package.json +++ b/.publish/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.2.28", + "version": "0.2.29", "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.28" + "@wrnexus/core": "^0.2.29" }, "files": [ "dist" diff --git a/.publish/i18n/package.json b/.publish/i18n/package.json index 4b044d6e..aa2a8a46 100644 --- a/.publish/i18n/package.json +++ b/.publish/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.2.28", + "version": "0.2.29", "type": "module", "description": "@wrnexus/i18n — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.28" + "@wrnexus/core": "^0.2.29" }, "files": [ "dist" diff --git a/.publish/jwt/package.json b/.publish/jwt/package.json index 64f50ba4..0087f5d0 100644 --- a/.publish/jwt/package.json +++ b/.publish/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.2.28", + "version": "0.2.29", "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 bf318a6c..e3efc4e3 100644 --- a/.publish/mobile/package.json +++ b/.publish/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.2.28", + "version": "0.2.29", "type": "module", "description": "@wrnexus/mobile — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/native": "^0.2.28" + "@wrnexus/native": "^0.2.29" }, "files": [ "dist" diff --git a/.publish/native/package.json b/.publish/native/package.json index fb04282e..74e22c8e 100644 --- a/.publish/native/package.json +++ b/.publish/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.2.28", + "version": "0.2.29", "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 ab71d43c..30577247 100644 --- a/.publish/oauth/package.json +++ b/.publish/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.2.28", + "version": "0.2.29", "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 aca2d2fe..44987175 100644 --- a/.publish/pubsub/package.json +++ b/.publish/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.2.28", + "version": "0.2.29", "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 652f0aed..f5081005 100644 --- a/.publish/queue/package.json +++ b/.publish/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.2.28", + "version": "0.2.29", "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 42cfb1d0..a259566f 100644 --- a/.publish/reactive/package.json +++ b/.publish/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.2.28", + "version": "0.2.29", "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 394e4410..f15718d0 100644 --- a/.publish/router/package.json +++ b/.publish/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.2.28", + "version": "0.2.29", "type": "module", "description": "@wrnexus/router — part of the WrNexus framework.", "license": "MIT", @@ -21,8 +21,8 @@ } }, "dependencies": { - "@wrnexus/compiler": "^0.2.28", - "@wrnexus/core": "^0.2.28" + "@wrnexus/compiler": "^0.2.29", + "@wrnexus/core": "^0.2.29" }, "files": [ "dist" diff --git a/.publish/ssr/package.json b/.publish/ssr/package.json index e710af69..0efd0938 100644 --- a/.publish/ssr/package.json +++ b/.publish/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.2.28", + "version": "0.2.29", "type": "module", "description": "@wrnexus/ssr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.28" + "@wrnexus/core": "^0.2.29" }, "files": [ "dist" diff --git a/.publish/styles/package.json b/.publish/styles/package.json index 6567fa60..2e34b7b7 100644 --- a/.publish/styles/package.json +++ b/.publish/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.2.28", + "version": "0.2.29", "type": "module", "description": "@wrnexus/styles — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/uploader": "^0.2.28" + "@wrnexus/uploader": "^0.2.29" }, "files": [ "dist" diff --git a/.publish/test/package.json b/.publish/test/package.json index d99671ac..6f92d47a 100644 --- a/.publish/test/package.json +++ b/.publish/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.2.28", + "version": "0.2.29", "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 a701a7d8..484110dc 100644 --- a/.publish/tracking/package.json +++ b/.publish/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.2.28", + "version": "0.2.29", "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 aca4e3d1..549b0f11 100644 --- a/.publish/ui/package.json +++ b/.publish/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.2.28", + "version": "0.2.29", "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.28" + "@wrnexus/core": "^0.2.29" }, "files": [ "dist", diff --git a/.publish/uploader/package.json b/.publish/uploader/package.json index 03a613eb..d8216971 100644 --- a/.publish/uploader/package.json +++ b/.publish/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.2.28", + "version": "0.2.29", "type": "module", "description": "@wrnexus/uploader — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.28" + "@wrnexus/core": "^0.2.29" }, "files": [ "dist" diff --git a/.publish/validation/package.json b/.publish/validation/package.json index af9595a8..e387ef84 100644 --- a/.publish/validation/package.json +++ b/.publish/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.2.28", + "version": "0.2.29", "type": "module", "description": "@wrnexus/validation — part of the WrNexus framework.", "license": "MIT", diff --git a/editors/vscode/package.json b/editors/vscode/package.json index bfda88c2..7c39087f 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -2,7 +2,7 @@ "name": "wrnexus", "displayName": "WRNexus Language Support", "description": "Syntax highlighting, formatting, diagnostics, snippets and completions for WrNexus .wrn files.", - "version": "0.2.5", + "version": "0.2.6", "publisher": "wrnexus", "private": true, "license": "SEE LICENSE IN LICENSE", diff --git a/editors/vscode/src/definition.js b/editors/vscode/src/definition.js index d4be17e7..9842f541 100644 --- a/editors/vscode/src/definition.js +++ b/editors/vscode/src/definition.js @@ -2,14 +2,10 @@ const vscode = require("vscode"); -const COMPONENT_DECLARATION = - /^\s*(component|layout)\s+([A-Za-z_$][\w$]*)\s*\{/gm; +const COMPONENT_DECLARATION = /^\s*(component|layout)\s+([A-Za-z_$][\w$]*)\s*\{/gm; function getTagAtPosition(document, position) { - const range = document.getWordRangeAtPosition( - position, - /[A-Za-z_$][\w$]*/, - ); + const range = document.getWordRangeAtPosition(position, /[A-Za-z_$][\w$]*/); if (!range) { return null; @@ -23,17 +19,13 @@ function getTagAtPosition(document, position) { const line = document.lineAt(position.line).text; const offset = document.offsetAt(position); - const lineStart = document.offsetAt( - new vscode.Position(position.line, 0), - ); + const lineStart = document.offsetAt(new vscode.Position(position.line, 0)); const characterOffset = offset - lineStart; const before = line.slice(0, characterOffset); const after = line.slice(characterOffset); - const insideTag = - before.lastIndexOf("<") > before.lastIndexOf(">") && - after.includes(">"); + const insideTag = before.lastIndexOf("<") > before.lastIndexOf(">") && after.includes(">"); if (!insideTag) { return null; @@ -75,21 +67,12 @@ async function findDeclaration(name) { continue; } - const nameOffset = - match.index + - match[0].indexOf(declarationName); + const nameOffset = match.index + match[0].indexOf(declarationName); const start = document.positionAt(nameOffset); - const end = document.positionAt( - nameOffset + declarationName.length, - ); + const end = document.positionAt(nameOffset + declarationName.length); - matches.push( - new vscode.Location( - uri, - new vscode.Range(start, end), - ), - ); + matches.push(new vscode.Location(uri, new vscode.Range(start, end))); } } @@ -131,4 +114,4 @@ module.exports = { getTagAtPosition, provideDefinition, registerDefinitionProvider, -}; \ No newline at end of file +}; diff --git a/editors/vscode/src/diagnostics.js b/editors/vscode/src/diagnostics.js index 01b1d245..ee27b798 100644 --- a/editors/vscode/src/diagnostics.js +++ b/editors/vscode/src/diagnostics.js @@ -5,14 +5,9 @@ const vscode = require("vscode"); const COLLECTION_NAME = "wrnexus"; const WRN_LANGUAGE_ID = "wrn"; -const TOP_LEVEL_PATTERN = - /^\s*(page|component|layout)\s+([A-Za-z_$][\w$]*)\s*\{/; +const TOP_LEVEL_PATTERN = /^\s*(page|component|layout)\s+([A-Za-z_$][\w$]*)\s*\{/; -const VALID_TOP_LEVEL_KINDS = new Set([ - "page", - "component", - "layout", -]); +const VALID_TOP_LEVEL_KINDS = new Set(["page", "component", "layout"]); const VALID_MEMBERS = { page: new Set([ @@ -28,21 +23,9 @@ const VALID_MEMBERS = { "realtime", ]), - component: new Set([ - "props", - "state", - "view", - "style", - "functions", - ]), + component: new Set(["props", "state", "view", "style", "functions"]), - layout: new Set([ - "props", - "state", - "view", - "style", - "functions", - ]), + layout: new Set(["props", "state", "view", "style", "functions"]), }; function createDiagnostic( @@ -54,10 +37,7 @@ function createDiagnostic( code, ) { const diagnostic = new vscode.Diagnostic( - new vscode.Range( - document.positionAt(startOffset), - document.positionAt(endOffset), - ), + new vscode.Range(document.positionAt(startOffset), document.positionAt(endOffset)), message, severity, ); @@ -80,11 +60,7 @@ function lineDiagnostic( ) { const line = document.lineAt(lineNumber); - const diagnostic = new vscode.Diagnostic( - line.range, - message, - severity, - ); + const diagnostic = new vscode.Diagnostic(line.range, message, severity); diagnostic.source = "WRNexus"; @@ -96,18 +72,14 @@ function lineDiagnostic( } function stripComments(source) { - return source.replace(//g, (comment) => - comment.replace(/[^\n]/g, " "), - ); + return source.replace(//g, (comment) => comment.replace(/[^\n]/g, " ")); } function findTopLevelDeclaration(document, source) { const match = TOP_LEVEL_PATTERN.exec(source); if (!match) { - const firstMeaningfulLine = source - .split(/\r?\n/) - .findIndex((line) => line.trim().length > 0); + const firstMeaningfulLine = source.split(/\r?\n/).findIndex((line) => line.trim().length > 0); return { diagnostic: lineDiagnostic( @@ -166,9 +138,7 @@ function validateBalancedCharacters(document, source) { continue; } - if ( - source.startsWith("", index + 4); if (commentEnd === -1) { @@ -190,11 +160,7 @@ function validateBalancedCharacters(document, source) { continue; } - if ( - character === "{" || - character === "[" || - character === "(" - ) { + if (character === "{" || character === "[" || character === "(") { stack.push({ character, offset: index, @@ -203,11 +169,7 @@ function validateBalancedCharacters(document, source) { continue; } - if ( - character === "}" || - character === "]" || - character === ")" - ) { + if (character === "}" || character === "]" || character === ")") { const expectedOpening = pairs[character]; const opening = stack.pop(); @@ -227,12 +189,7 @@ function validateBalancedCharacters(document, source) { } for (const opening of stack) { - const expectedClosing = - opening.character === "{" - ? "}" - : opening.character === "[" - ? "]" - : ")"; + const expectedClosing = opening.character === "{" ? "}" : opening.character === "[" ? "]" : ")"; diagnostics.push( createDiagnostic( @@ -284,8 +241,7 @@ function validateHtmlTags(document, source) { ]); const cleaned = stripComments(source); - const tagPattern = - /<\/?([A-Za-z][A-Za-z0-9_$:.-]*)(?:\s[\s\S]*?)?\/?>/g; + const tagPattern = /<\/?([A-Za-z][A-Za-z0-9_$:.-]*)(?:\s[\s\S]*?)?\/?>/g; let match; @@ -358,18 +314,13 @@ function validateHtmlTags(document, source) { } function getRootBodyRange(source, rootMatch) { - const openingBrace = - rootMatch.index + rootMatch[0].lastIndexOf("{"); + const openingBrace = rootMatch.index + rootMatch[0].lastIndexOf("{"); let depth = 0; let quote = null; let escaped = false; - for ( - let index = openingBrace; - index < source.length; - index += 1 - ) { + for (let index = openingBrace; index < source.length; index += 1) { const character = source[index]; if (quote !== null) { @@ -425,6 +376,12 @@ function findRootMembers(source, bodyStart, bodyEnd) { let quote = null; let escaped = false; + const skipLine = () => { + while (index < bodyEnd && source[index] !== "\n" && source[index] !== "\r") { + index += 1; + } + }; + while (index < bodyEnd) { const character = source[index]; @@ -473,17 +430,11 @@ function findRootMembers(source, bodyStart, bodyEnd) { continue; } - if ( - depth === 0 && - /[A-Za-z_]/.test(character) - ) { + if (depth === 0 && /[A-Za-z_]/.test(character)) { const start = index; index += 1; - while ( - index < bodyEnd && - /[A-Za-z0-9_-]/.test(source[index]) - ) { + while (index < bodyEnd && /[A-Za-z0-9_-]/.test(source[index])) { index += 1; } @@ -495,6 +446,11 @@ function findRootMembers(source, bodyStart, bodyEnd) { end: index, }); + // Single-line declarations must skip the rest of the line. + if (name === "state" || name === "layout") { + skipLine(); + } + continue; } @@ -504,12 +460,7 @@ function findRootMembers(source, bodyStart, bodyEnd) { return members; } -function validateRootMembers( - document, - source, - rootKind, - rootMatch, -) { +function validateRootMembers(document, source, rootKind, rootMatch) { const diagnostics = []; const allowed = VALID_MEMBERS[rootKind]; @@ -518,11 +469,7 @@ function validateRootMembers( } const bodyRange = getRootBodyRange(source, rootMatch); - const members = findRootMembers( - source, - bodyRange.start, - bodyRange.end, - ); + const members = findRootMembers(source, bodyRange.start, bodyRange.end); for (const member of members) { if (allowed.has(member.name)) { @@ -544,12 +491,7 @@ function validateRootMembers( return diagnostics; } -function validateRequiredView( - document, - source, - rootKind, - rootMatch, -) { +function validateRequiredView(document, source, rootKind, rootMatch) { const bodyRange = getRootBodyRange(source, rootMatch); const body = source.slice(bodyRange.start, bodyRange.end); @@ -569,17 +511,10 @@ function validateRequiredView( ]; } -function validateLayoutUsage( - document, - source, - rootKind, -) { +function validateLayoutUsage(document, source, rootKind) { const diagnostics = []; - if ( - rootKind !== "page" && - /^\s*layout\s*=/m.test(source) - ) { + if (rootKind !== "page" && /^\s*layout\s*=/m.test(source)) { const match = /^\s*layout\s*=/m.exec(source); if (match) { @@ -588,7 +523,7 @@ function validateLayoutUsage( document, match.index, match.index + match[0].length, - "`layout = \"...\"` is only valid inside a page.", + '`layout = "..."` is only valid inside a page.', vscode.DiagnosticSeverity.Error, "wrn-invalid-layout-member", ), @@ -611,16 +546,11 @@ function validateDocument(document) { } const diagnostics = []; - const declaration = findTopLevelDeclaration( - document, - source, - ); + const declaration = findTopLevelDeclaration(document, source); if (declaration.diagnostic) { diagnostics.push(declaration.diagnostic); - diagnostics.push( - ...validateBalancedCharacters(document, source), - ); + diagnostics.push(...validateBalancedCharacters(document, source)); return diagnostics; } @@ -630,8 +560,7 @@ function validateDocument(document) { createDiagnostic( document, declaration.match.index, - declaration.match.index + - declaration.match[0].length, + declaration.match.index + declaration.match[0].length, `Unsupported WRN declaration \`${declaration.kind}\`.`, vscode.DiagnosticSeverity.Error, "wrn-invalid-kind", @@ -641,48 +570,21 @@ function validateDocument(document) { return diagnostics; } - diagnostics.push( - ...validateBalancedCharacters(document, source), - ); + diagnostics.push(...validateBalancedCharacters(document, source)); - diagnostics.push( - ...validateHtmlTags(document, source), - ); + diagnostics.push(...validateHtmlTags(document, source)); - diagnostics.push( - ...validateRootMembers( - document, - source, - declaration.kind, - declaration.match, - ), - ); + diagnostics.push(...validateRootMembers(document, source, declaration.kind, declaration.match)); - diagnostics.push( - ...validateRequiredView( - document, - source, - declaration.kind, - declaration.match, - ), - ); + diagnostics.push(...validateRequiredView(document, source, declaration.kind, declaration.match)); - diagnostics.push( - ...validateLayoutUsage( - document, - source, - declaration.kind, - ), - ); + diagnostics.push(...validateLayoutUsage(document, source, declaration.kind)); return diagnostics; } function registerDiagnostics(context) { - const collection = - vscode.languages.createDiagnosticCollection( - COLLECTION_NAME, - ); + const collection = vscode.languages.createDiagnosticCollection(COLLECTION_NAME); const timers = new Map(); @@ -691,9 +593,7 @@ function registerDiagnostics(context) { return; } - const previousTimer = timers.get( - document.uri.toString(), - ); + const previousTimer = timers.get(document.uri.toString()); if (previousTimer) { clearTimeout(previousTimer); @@ -702,10 +602,7 @@ function registerDiagnostics(context) { const timer = setTimeout(() => { timers.delete(document.uri.toString()); - collection.set( - document.uri, - validateDocument(document), - ); + collection.set(document.uri, validateDocument(document)); }, 150); timers.set(document.uri.toString(), timer); @@ -726,19 +623,17 @@ function registerDiagnostics(context) { vscode.workspace.onDidSaveTextDocument(update), - vscode.workspace.onDidCloseTextDocument( - (document) => { - const key = document.uri.toString(); - const timer = timers.get(key); + vscode.workspace.onDidCloseTextDocument((document) => { + const key = document.uri.toString(); + const timer = timers.get(key); - if (timer) { - clearTimeout(timer); - timers.delete(key); - } + if (timer) { + clearTimeout(timer); + timers.delete(key); + } - collection.delete(document.uri); - }, - ), + collection.delete(document.uri); + }), { dispose() { @@ -758,4 +653,4 @@ module.exports = { validateDocument, validateHtmlTags, validateRootMembers, -}; \ No newline at end of file +}; diff --git a/editors/vscode/wrnexus-0.2.5.vsix b/editors/vscode/wrnexus-0.2.5.vsix index 17333bc2a7bc90fd058db27df994a1f708be7f00..68c3c4d2725105b98c380a11448cd55e73a6b506 100644 GIT binary patch delta 3666 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 diff --git a/editors/vscode/wrnexus-0.2.6.vsix b/editors/vscode/wrnexus-0.2.6.vsix new file mode 100644 index 0000000000000000000000000000000000000000..692c28647da9d55478528a48360921c3f963d51e GIT binary patch literal 40722 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%0D3u;mKrSxl-}q^y<` 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.. + * Example: subdomain "sso" + rootDomain "local.wrnexus.test". + */ + subdomain?: string; + + /** + * Explicit hostname overrides by environment. + */ + domains?: string[] | Partial>; + + /** + * Complete public URL override by environment. + */ + urls?: Partial>; + port?: number; - /** Per-app access control enforced at the gateway (basic auth, IP allowlist, forward-auth). */ auth?: GatewayAuth; } + export interface WorkspaceConfig { + defaultEnvironment?: WorkspaceEnvironment; + + environments?: Record; + apps: WorkspaceApp[]; - /** Gateway-wide security (trusted hosts, rate limit, headers, access log). */ security?: GatewaySecurity; } @@ -263,7 +290,13 @@ export async function addWorkspaceApp(root: string, name: string, args: string[] if (config.apps.some((app) => app.name === name || app.dir.replace(/\\/g, "/") === dir)) { throw new Error(`Workspace app '${name}' already exists.`); } - if (config.apps.some((app) => app.domains.includes(domain))) { + const normalizedDomain = domain.toLowerCase(); + + if ( + config.apps.some((app) => + configuredDomains(app).some((configured) => configured.toLowerCase() === normalizedDomain), + ) + ) { throw new Error(`Workspace domain '${domain}' is already assigned.`); } const appRoot = join(resolvedRoot, "apps", name); @@ -285,6 +318,24 @@ export async function addWorkspaceApp(root: string, name: string, args: string[] console.log("\nRun `bun install`, then `bun run dev`."); } +function configuredDomains(app: WorkspaceApp): string[] { + if (!app.domains) { + return []; + } + + if (Array.isArray(app.domains)) { + return app.domains; + } + + return Object.values(app.domains).flatMap((value) => { + if (!value) { + return []; + } + + return Array.isArray(value) ? value : [value]; + }); +} + /** Scaffold a monorepo workspace with two starter apps + a shared package. */ export function createWorkspace(name: string): void { if (!name) { @@ -329,24 +380,171 @@ export async function loadWorkspaceConfig(root: string): Promise { const config = await loadWorkspaceConfig(resolve(root)); - const portArg = args.find((a) => a.startsWith("--port=")); - const hostArg = args.find((a) => a.startsWith("--host=")); - const port = portArg ? Number(portArg.split("=")[1]) : 3000; + + const portArg = args.find((arg) => arg.startsWith("--port=")); + + const hostArg = args.find((arg) => arg.startsWith("--host=")); + + const environmentArg = args.find((arg) => arg.startsWith("--environment=")); + + const profileArg = args.find((arg) => arg.startsWith("--profile=")); + + const environment = + environmentArg?.split("=")[1] || + profileArg?.split("=")[1] || + process.env.WRNEXUS_ENV || + process.env.WRNEXUS_PROFILE || + config.defaultEnvironment || + (args.includes("--prod") ? "production" : "development"); + + const resolved = resolveWorkspaceConfig(config, environment); + + const port = portArg + ? Number(portArg.split("=")[1]) + : resolved.environment === "development" + ? 3000 + : 80; + const hostname = hostArg?.split("=")[1]; - const mode = args.includes("--prod") ? "production" : "development"; const { startGateway } = await import("@wrnexus/dev-server"); + await startGateway({ port, hostname, - mode, - security: config.security, - apps: config.apps.map((a) => ({ - name: a.name, - dir: resolve(root, a.dir), - domains: a.domains, - port: a.port, - auth: a.auth, + mode: environment === "production" ? "production" : "development", + environment, + security: resolved.security, + apps: resolved.apps.map((app) => ({ + name: app.name, + dir: resolve(root, app.dir), + domains: app.domains, + publicOrigin: app.publicOrigin, + port: app.port, + auth: app.auth, })), }); } + +function environmentVariableName(appName: string): string { + return `WRNEXUS_APP_${appName.replace(/[^A-Za-z0-9]/g, "_").toUpperCase()}_URL`; +} + +function normalizeOrigin(value: string): string { + const url = new URL(value); + + if (url.protocol !== "http:" && url.protocol !== "https:") { + throw new Error(`Application URL must use http or https: ${value}`); + } + + return url.origin; +} + +function valuesForEnvironment( + value: WorkspaceApp["domains"], + environment: WorkspaceEnvironment, +): string[] { + if (!value) return []; + + if (Array.isArray(value)) { + return value; + } + + const selected = value[environment]; + + if (!selected) return []; + + return Array.isArray(selected) ? selected : [selected]; +} + +export interface ResolvedWorkspaceApp extends WorkspaceApp { + domains: string[]; + publicOrigin: string; +} + +export interface ResolvedWorkspaceConfig { + environment: WorkspaceEnvironment; + apps: ResolvedWorkspaceApp[]; + security?: GatewaySecurity; +} + +export function resolveWorkspaceConfig( + config: WorkspaceConfig, + environment: WorkspaceEnvironment, +): ResolvedWorkspaceConfig { + const environmentConfig = config.environments?.[environment] ?? {}; + + const protocol = environmentConfig.protocol ?? (environment === "production" ? "https" : "http"); + + const configuredPort = environmentConfig.port; + + const apps = config.apps.map((app) => { + const envOverride = process.env[environmentVariableName(app.name)]; + + const configuredUrl = app.urls?.[environment]; + + const explicitDomains = valuesForEnvironment(app.domains, environment); + + const generatedDomain = + app.subdomain && environmentConfig.rootDomain + ? `${app.subdomain}.${environmentConfig.rootDomain}` + : undefined; + + const domains = + explicitDomains.length > 0 ? explicitDomains : generatedDomain ? [generatedDomain] : []; + + let publicOrigin: string; + + if (envOverride) { + publicOrigin = normalizeOrigin(envOverride); + } else if (configuredUrl) { + publicOrigin = normalizeOrigin(configuredUrl); + } else if (domains[0]) { + const port = + configuredPort && + !( + (protocol === "http" && configuredPort === 80) || + (protocol === "https" && configuredPort === 443) + ) + ? `:${configuredPort}` + : ""; + + publicOrigin = `${protocol}://${domains[0]}${port}`; + } else { + throw new Error( + `No domain or URL configured for workspace app "${app.name}" in environment "${environment}".`, + ); + } + + const publicUrl = new URL(publicOrigin); + + return { + ...app, + domains: domains.length > 0 ? domains : [publicUrl.hostname], + publicOrigin, + }; + }); + + const usedDomains = new Map(); + + for (const app of apps) { + for (const domain of app.domains) { + const normalized = domain.toLowerCase(); + const owner = usedDomains.get(normalized); + + if (owner) { + throw new Error( + `Workspace domain "${domain}" is assigned to both "${owner}" and "${app.name}".`, + ); + } + + usedDomains.set(normalized, app.name); + } + } + + return { + environment, + apps, + security: config.security, + }; +} diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 9e408bb8..93fbe60b 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.2.28", + "version": "0.2.29", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/core/package.json b/packages/core/package.json index 5063a060..81ead3d2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.2.28", + "version": "0.2.29", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/csr/package.json b/packages/csr/package.json index 451084c9..6d929aaf 100644 --- a/packages/csr/package.json +++ b/packages/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.2.28", + "version": "0.2.29", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/db/package.json b/packages/db/package.json index 0dcbe1b2..fe5cdf60 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.2.28", + "version": "0.2.29", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json index 6ecd932e..4ed22b4e 100644 --- a/packages/dev-server/package.json +++ b/packages/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.2.28", + "version": "0.2.29", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/dev-server/src/gateway.ts b/packages/dev-server/src/gateway.ts index 15425736..f0592c3b 100644 --- a/packages/dev-server/src/gateway.ts +++ b/packages/dev-server/src/gateway.ts @@ -35,6 +35,7 @@ export interface GatewayApp { dir: string; /** Host names routed to this app (e.g. ["localhost", "web.localhost"]). */ domains: string[]; + publicOrigin?: string; /** Optional fixed internal port; otherwise assigned from the gateway port. */ port?: number; /** Access control enforced at the edge for this app. */ @@ -59,6 +60,7 @@ export interface GatewayOptions { port?: number; hostname?: string; mode?: "development" | "production"; + environment?: string; apps: GatewayApp[]; security?: GatewaySecurity; } @@ -79,7 +81,7 @@ interface WsBridge { origin: string; path: string; backend?: WebSocket; - queue: (string | ArrayBufferLike | ArrayBufferView)[]; + queue: Array; } /** Decide whether a gateway child should be relaunched after it exits. */ @@ -262,6 +264,11 @@ export async function startGateway(opts: GatewayOptions): Promise [app.name, app.publicOrigin ?? `http://${app.domains[0]}:${port}`]), + ); const displayHost = hostname === "0.0.0.0" || hostname === "::" ? "localhost" : hostname; const serveEntry = fileURLToPath(import.meta.resolve("@wrnexus/dev-server/serve-entry")); @@ -281,12 +288,28 @@ export async function startGateway(opts: GatewayOptions): Promise ws.send(e.data as string | ArrayBufferLike)); + backend.addEventListener("message", async (event) => { + const data = event.data; + + if (typeof data === "string") { + ws.send(data); + return; + } + + if (data instanceof Blob) { + const buffer = await data.arrayBuffer(); + ws.send(buffer); + return; + } + + if (data instanceof ArrayBuffer) { + ws.send(data); + return; + } + + if (ArrayBuffer.isView(data)) { + const buffer = data.buffer.slice( + data.byteOffset, + data.byteOffset + data.byteLength, + ) as ArrayBuffer; + + ws.send(buffer); + } + }); backend.addEventListener("close", () => ws.close()); backend.addEventListener("error", () => ws.close()); }, message(ws, message) { + let data: string | ArrayBuffer; + + if (typeof message === "string") { + data = message; + } else if (message instanceof ArrayBuffer) { + data = message; + } else { + const view = message as ArrayBufferView; + + data = view.buffer.slice( + view.byteOffset, + view.byteOffset + view.byteLength, + ) as ArrayBuffer; + } + const backend = ws.data.backend; - if (backend && backend.readyState === WebSocket.OPEN) backend.send(message); - else ws.data.queue.push(message); + + if (backend && backend.readyState === WebSocket.OPEN) { + backend.send(data); + } else { + ws.data.queue.push(data); + } }, close(ws) { ws.data.backend?.close(); diff --git a/packages/encryption/package.json b/packages/encryption/package.json index 43127462..8874108a 100644 --- a/packages/encryption/package.json +++ b/packages/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.2.28", + "version": "0.2.29", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/helpers/package.json b/packages/helpers/package.json index 7e5fbcce..a5b4a43f 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.2.28", + "version": "0.2.29", "private": true, "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", diff --git a/packages/helpers/src/index.ts b/packages/helpers/src/index.ts index 33a52a31..91b80f33 100644 --- a/packages/helpers/src/index.ts +++ b/packages/helpers/src/index.ts @@ -136,3 +136,11 @@ export function redirectToLogin( target.searchParams.set(options.returnToParam ?? "returnTo", original.href); return Response.redirect(target, options.status ?? 302); } + +export { + appOrigin, + appUrl, + currentAppName, + currentAppOrigin, + workspaceAppOrigins, +} from "./workspace.ts"; diff --git a/packages/helpers/src/workspace.ts b/packages/helpers/src/workspace.ts new file mode 100644 index 00000000..ca55bb49 --- /dev/null +++ b/packages/helpers/src/workspace.ts @@ -0,0 +1,58 @@ +function workspaceOrigins(): Record { + const value = process.env.WRNEXUS_WORKSPACE_ORIGINS; + + if (!value) { + return {}; + } + + try { + const parsed = JSON.parse(value); + + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return {}; + } + + return parsed as Record; + } catch { + return {}; + } +} + +function normalizePath(path: string): string { + if (!path) return "/"; + + return path.startsWith("/") ? path : `/${path}`; +} + +export function appOrigin(appName: string): string { + const origins = workspaceOrigins(); + const origin = origins[appName]; + + if (!origin) { + throw new Error( + `Unknown workspace app "${appName}". Available apps: ${ + Object.keys(origins).join(", ") || "none" + }.`, + ); + } + + return new URL(origin).origin; +} + +export function appUrl(appName: string, path = "/"): string { + return new URL(normalizePath(path), `${appOrigin(appName)}/`).href; +} + +export function currentAppName(): string | undefined { + return process.env.WRNEXUS_APP_NAME; +} + +export function currentAppOrigin(): string | undefined { + const value = process.env.WRNEXUS_APP_ORIGIN; + + return value ? new URL(value).origin : undefined; +} + +export function workspaceAppOrigins(): Readonly> { + return Object.freeze({ ...workspaceOrigins() }); +} diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 7d8d7f8b..41da8a44 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.2.28", + "version": "0.2.29", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/jwt/package.json b/packages/jwt/package.json index d5f46105..82073a7d 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.2.28", + "version": "0.2.29", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/mobile/package.json b/packages/mobile/package.json index bbe78391..17047d85 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.2.28", + "version": "0.2.29", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/native/package.json b/packages/native/package.json index 7b909e02..9ddc61ab 100644 --- a/packages/native/package.json +++ b/packages/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.2.28", + "version": "0.2.29", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/oauth/package.json b/packages/oauth/package.json index 987437eb..3371fe0a 100644 --- a/packages/oauth/package.json +++ b/packages/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.2.28", + "version": "0.2.29", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/pubsub/package.json b/packages/pubsub/package.json index d71fda71..7fb273d0 100644 --- a/packages/pubsub/package.json +++ b/packages/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.2.28", + "version": "0.2.29", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/queue/package.json b/packages/queue/package.json index 95fe539f..679f783d 100644 --- a/packages/queue/package.json +++ b/packages/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.2.28", + "version": "0.2.29", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/reactive/package.json b/packages/reactive/package.json index bb611b35..33d6f626 100644 --- a/packages/reactive/package.json +++ b/packages/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.2.28", + "version": "0.2.29", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/router/package.json b/packages/router/package.json index 8b94485c..496e6080 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.2.28", + "version": "0.2.29", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/ssr/package.json b/packages/ssr/package.json index bc7c755a..ac49eceb 100644 --- a/packages/ssr/package.json +++ b/packages/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.2.28", + "version": "0.2.29", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/styles/package.json b/packages/styles/package.json index 9be35def..bd78691b 100644 --- a/packages/styles/package.json +++ b/packages/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.2.28", + "version": "0.2.29", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/test/package.json b/packages/test/package.json index f901b71b..17f3a3b3 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.2.28", + "version": "0.2.29", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/tracking/package.json b/packages/tracking/package.json index ccb0a3de..86ff9254 100644 --- a/packages/tracking/package.json +++ b/packages/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.2.28", + "version": "0.2.29", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index a8c9ca60..8e6ccb88 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.2.28", + "version": "0.2.29", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/uploader/package.json b/packages/uploader/package.json index 07b29cab..ec0de20e 100644 --- a/packages/uploader/package.json +++ b/packages/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.2.28", + "version": "0.2.29", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/validation/package.json b/packages/validation/package.json index 0d53a7b4..315b86f6 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.2.28", + "version": "0.2.29", "private": true, "type": "module", "main": "src/index.ts",