feat: restore gateway HMR and add WRN formatting
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ai",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/authz",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/authz — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
+11
-11
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/cli",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"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.18",
|
||||
"@wrnexus/router": "^0.2.18",
|
||||
"@wrnexus/csr": "^0.2.18",
|
||||
"@wrnexus/compiler": "^0.2.18",
|
||||
"@wrnexus/styles": "^0.2.18",
|
||||
"@wrnexus/dev-server": "^0.2.18",
|
||||
"@wrnexus/ui": "^0.2.18",
|
||||
"@wrnexus/validation": "^0.2.18",
|
||||
"@wrnexus/i18n": "^0.2.18",
|
||||
"@wrnexus/db": "^0.2.18"
|
||||
"@wrnexus/core": "^0.2.19",
|
||||
"@wrnexus/router": "^0.2.19",
|
||||
"@wrnexus/csr": "^0.2.19",
|
||||
"@wrnexus/compiler": "^0.2.19",
|
||||
"@wrnexus/styles": "^0.2.19",
|
||||
"@wrnexus/dev-server": "^0.2.19",
|
||||
"@wrnexus/ui": "^0.2.19",
|
||||
"@wrnexus/validation": "^0.2.19",
|
||||
"@wrnexus/i18n": "^0.2.19",
|
||||
"@wrnexus/db": "^0.2.19"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/compiler",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/compiler — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/core",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/core — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/csr",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/csr — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.2.18"
|
||||
"@wrnexus/core": "^0.2.19"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/db",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/db — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -142,7 +142,7 @@ interface ProdOptions {
|
||||
|
||||
### `startGateway(opts)` — multi-app gateway
|
||||
|
||||
Serves several apps behind one port and routes each request to the right app by its `Host` header. Each app runs as its own child process (full isolation); the gateway is a thin host-based reverse proxy for HTTP and WebSocket. Apps communicate at runtime via `@wrnexus/pubsub` (use the Redis driver so messages cross processes).
|
||||
Serves several apps behind one port and routes each request to the right app by its `Host` header. Each app runs as its own child process (full isolation); the gateway is a thin host-based reverse proxy for HTTP and WebSocket. In development, the gateway supervises every child: editing a page, component, API, or middleware restarts only that app and the HMR client reconnects to display the latest page. Apps communicate at runtime via `@wrnexus/pubsub` (use the Redis driver so messages cross processes).
|
||||
|
||||
```ts
|
||||
interface GatewayOptions {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-server",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/dev-server — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -25,18 +25,18 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.2.18",
|
||||
"@wrnexus/router": "^0.2.18",
|
||||
"@wrnexus/ssr": "^0.2.18",
|
||||
"@wrnexus/csr": "^0.2.18",
|
||||
"@wrnexus/compiler": "^0.2.18",
|
||||
"@wrnexus/styles": "^0.2.18",
|
||||
"@wrnexus/ui": "^0.2.18",
|
||||
"@wrnexus/validation": "^0.2.18",
|
||||
"@wrnexus/i18n": "^0.2.18",
|
||||
"@wrnexus/db": "^0.2.18",
|
||||
"@wrnexus/pubsub": "^0.2.18",
|
||||
"@wrnexus/uploader": "^0.2.18"
|
||||
"@wrnexus/core": "^0.2.19",
|
||||
"@wrnexus/router": "^0.2.19",
|
||||
"@wrnexus/ssr": "^0.2.19",
|
||||
"@wrnexus/csr": "^0.2.19",
|
||||
"@wrnexus/compiler": "^0.2.19",
|
||||
"@wrnexus/styles": "^0.2.19",
|
||||
"@wrnexus/ui": "^0.2.19",
|
||||
"@wrnexus/validation": "^0.2.19",
|
||||
"@wrnexus/i18n": "^0.2.19",
|
||||
"@wrnexus/db": "^0.2.19",
|
||||
"@wrnexus/pubsub": "^0.2.19",
|
||||
"@wrnexus/uploader": "^0.2.19"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/encryption",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/encryption — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/helpers",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"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.18"
|
||||
"@wrnexus/core": "^0.2.19"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/i18n",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/i18n — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.2.18"
|
||||
"@wrnexus/core": "^0.2.19"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/jwt",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/jwt — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/mobile",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/mobile — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/native": "^0.2.18"
|
||||
"@wrnexus/native": "^0.2.19"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/native",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/native — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/oauth",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/oauth — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/pubsub",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/pubsub — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/queue",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/queue — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/reactive",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/reactive — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/router",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/router — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -21,8 +21,8 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/compiler": "^0.2.18",
|
||||
"@wrnexus/core": "^0.2.18"
|
||||
"@wrnexus/compiler": "^0.2.19",
|
||||
"@wrnexus/core": "^0.2.19"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ssr",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/ssr — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.2.18"
|
||||
"@wrnexus/core": "^0.2.19"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/styles",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/styles — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/uploader": "^0.2.18"
|
||||
"@wrnexus/uploader": "^0.2.19"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/test",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/test — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/tracking",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/tracking — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ui",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"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.18"
|
||||
"@wrnexus/core": "^0.2.19"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/uploader",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/uploader — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.2.18"
|
||||
"@wrnexus/core": "^0.2.19"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/validation",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/validation — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## 0.2.0
|
||||
|
||||
- Added native Format Document and format-on-save support for `.wrn` files.
|
||||
- Added configurable formatter enablement through `wrnexus.format.enable`.
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- Initial Marketplace package for WrNexus `.wrn` language support.
|
||||
|
||||
@@ -21,6 +21,8 @@ Bun-powered full-stack framework.
|
||||
`entity.other.attribute-name.wrn.directive` and `…wire.event`).
|
||||
- **Diagnostics.** Parse errors from the real `@wrnexus/compiler` are shown inline
|
||||
as you type, anchored to the exact offset.
|
||||
- **Formatting.** Format Document and format-on-save normalize WRN block, HTML,
|
||||
CSS, and function indentation without rewriting application expressions.
|
||||
- **Snippets.** `page`, `component`, `view`, `state`, `props`, `seo`, `api`,
|
||||
`ssr`, `client`, `realtime`, `functions`, `style`, plus view helpers `mount`,
|
||||
`for`, `show`, `t`.
|
||||
@@ -29,9 +31,10 @@ Bun-powered full-stack framework.
|
||||
|
||||
## Settings
|
||||
|
||||
| Setting | Default | Description |
|
||||
| ---------------------------- | ------- | ----------------------------------- |
|
||||
| `wrnexus.diagnostics.enable` | `true` | Show parse errors for `.wrn` files. |
|
||||
| Setting | Default | Description |
|
||||
| ---------------------------- | ------- | ----------------------------------------- |
|
||||
| `wrnexus.diagnostics.enable` | `true` | Show parse errors for `.wrn` files. |
|
||||
| `wrnexus.format.enable` | `true` | Enable the built-in formatter for `.wrn`. |
|
||||
|
||||
## Building the diagnostics engine
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wrnexus",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wrnexus",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@vscode/vsce": "^3.9.2"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "wrnexus",
|
||||
"displayName": "WrNexus (.wrn)",
|
||||
"description": "Syntax highlighting, diagnostics, snippets and completions for WrNexus .wrn files.",
|
||||
"version": "0.1.0",
|
||||
"description": "Syntax highlighting, formatting, diagnostics, snippets and completions for WrNexus .wrn files.",
|
||||
"version": "0.2.0",
|
||||
"publisher": "wrnexus",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
@@ -25,7 +25,8 @@
|
||||
"categories": [
|
||||
"Programming Languages",
|
||||
"Snippets",
|
||||
"Linters"
|
||||
"Linters",
|
||||
"Formatters"
|
||||
],
|
||||
"keywords": [
|
||||
"wire",
|
||||
@@ -90,6 +91,11 @@
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Show parse errors for .wrn files as you type."
|
||||
},
|
||||
"wrnexus.format.enable": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Enable the built-in formatter for .wrn files."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -40,10 +40,13 @@ var __export = (target, all) => {
|
||||
var exports_src = {};
|
||||
__export(exports_src, {
|
||||
parse: () => parse,
|
||||
generateNative: () => generateNative,
|
||||
generate: () => generate,
|
||||
compileWireFile: () => compileWireFile,
|
||||
compileNativeWireFile: () => compileNativeWireFile,
|
||||
compile: () => compile,
|
||||
ParseError: () => ParseError,
|
||||
NativeCompileError: () => NativeCompileError,
|
||||
Lexer: () => Lexer,
|
||||
LexError: () => LexError
|
||||
});
|
||||
@@ -678,7 +681,7 @@ function attrValue(attrs, name) {
|
||||
}
|
||||
function renderAttr(attr) {
|
||||
if (attr.event)
|
||||
return ` data-on-${attr.name}="${attrEscape(attr.value)}"`;
|
||||
return ` ${eventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
||||
switch (attr.name) {
|
||||
case "api":
|
||||
case "ssrGet":
|
||||
@@ -690,6 +693,13 @@ function renderAttr(attr) {
|
||||
return attr.boolean ? ` ${attr.name}` : ` ${attr.name}="${attrEscape(attr.value)}"`;
|
||||
}
|
||||
}
|
||||
function eventAttribute(name) {
|
||||
if (name.startsWith("browser-"))
|
||||
return `data-on-wrnexus-browser-${name.slice(8)}`;
|
||||
if (name.startsWith("mobile-"))
|
||||
return `data-on-wrnexus-mobile-${name.slice(7)}`;
|
||||
return `data-on-${name}`;
|
||||
}
|
||||
function renderAttrs(attrs, csrId) {
|
||||
const rendered = attrs.map(renderAttr).join("");
|
||||
return csrId ? `${rendered} data-wrnexus-csr="${attrEscape(csrId)}"` : rendered;
|
||||
@@ -768,7 +778,7 @@ function renderLoopBody(node) {
|
||||
if (node.type === "if")
|
||||
return compileIfExpr(node);
|
||||
const attrs = node.attrs.map((a) => {
|
||||
const name = a.event ? `data-on-${a.name}` : a.name;
|
||||
const name = a.event ? eventAttribute(a.name) : a.name;
|
||||
if (a.boolean)
|
||||
return escLit(` ${name}`);
|
||||
return escLit(` ${name}="`) + bakeLoopAttr(a.value) + escLit(`"`);
|
||||
@@ -1176,7 +1186,7 @@ function renderComponentNode(node, 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).");
|
||||
}
|
||||
const attrs = node.attrs.map((a) => a.event ? ` data-on-${a.name}="${compileAttrValue(a.value, ctx)}"` : a.boolean ? ` ${a.name}` : ` ${a.name}="${compileAttrValue(a.value, ctx)}"`).join("");
|
||||
const attrs = node.attrs.map((a) => a.event ? ` ${eventAttribute(a.name)}="${compileAttrValue(a.value, ctx)}"` : a.boolean ? ` ${a.name}` : ` ${a.name}="${compileAttrValue(a.value, ctx)}"`).join("");
|
||||
const loops = loopVarsOf(node);
|
||||
const childCtx = loops.length > 0 ? { ...ctx, loopVars: new Set([...ctx.loopVars ?? [], ...loops]) } : ctx;
|
||||
if (VOID_ELEMENTS.has(node.tag.toLowerCase()))
|
||||
@@ -1262,7 +1272,211 @@ function __wireAttr(v: any): string {
|
||||
`;
|
||||
}
|
||||
|
||||
// ../../packages/compiler/src/native-codegen.ts
|
||||
class NativeCompileError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "NativeCompileError";
|
||||
}
|
||||
}
|
||||
var tagMap = {
|
||||
div: "View",
|
||||
main: "View",
|
||||
section: "View",
|
||||
article: "View",
|
||||
nav: "View",
|
||||
header: "View",
|
||||
footer: "View",
|
||||
aside: "View",
|
||||
form: "View",
|
||||
ul: "View",
|
||||
ol: "View",
|
||||
li: "View",
|
||||
p: "Text",
|
||||
span: "Text",
|
||||
strong: "Text",
|
||||
em: "Text",
|
||||
small: "Text",
|
||||
label: "Text",
|
||||
h1: "Text",
|
||||
h2: "Text",
|
||||
h3: "Text",
|
||||
h4: "Text",
|
||||
h5: "Text",
|
||||
h6: "Text",
|
||||
button: "Pressable",
|
||||
a: "Pressable",
|
||||
input: "TextInput",
|
||||
textarea: "TextInput",
|
||||
img: "Image",
|
||||
view: "View",
|
||||
text: "Text",
|
||||
pressable: "Pressable",
|
||||
textinput: "TextInput",
|
||||
image: "Image",
|
||||
scrollview: "ScrollView",
|
||||
safeareaview: "SafeAreaView",
|
||||
flatlist: "FlatList",
|
||||
activityindicator: "ActivityIndicator"
|
||||
};
|
||||
var attrMap = {
|
||||
class: "style",
|
||||
className: "style",
|
||||
src: "source",
|
||||
alt: "accessibilityLabel",
|
||||
placeholder: "placeholder",
|
||||
disabled: "disabled",
|
||||
value: "value",
|
||||
href: "__href",
|
||||
"aria-label": "accessibilityLabel"
|
||||
};
|
||||
function expression(value) {
|
||||
const exact = /^\{([\s\S]+)\}$/.exec(value.trim());
|
||||
return exact?.[1]?.trim() ?? null;
|
||||
}
|
||||
function textJsx(value) {
|
||||
const pieces = [];
|
||||
let last = 0;
|
||||
for (const match of value.matchAll(/\{([^{}]+)\}/g)) {
|
||||
if (match.index > last)
|
||||
pieces.push(value.slice(last, match.index));
|
||||
const expr = match[1].trim();
|
||||
pieces.push(expr.startsWith("t:") ? `{${JSON.stringify(expr.slice(2).trim())}}` : `{${expr}}`);
|
||||
last = match.index + match[0].length;
|
||||
}
|
||||
pieces.push(value.slice(last));
|
||||
return pieces.join("").replace(/([<>])/g, (char) => char === "<" ? "<" : ">");
|
||||
}
|
||||
function eventBody(value, states) {
|
||||
let body = expression(value) ?? value;
|
||||
for (const state of states) {
|
||||
const cap = state[0].toUpperCase() + state.slice(1);
|
||||
body = body.replace(new RegExp(`\\b${state}\\+\\+`, "g"), `set${cap}(value => value + 1)`).replace(new RegExp(`\\b${state}--`, "g"), `set${cap}(value => value - 1)`).replace(new RegExp(`\\b${state}\\s*=\\s*([^;]+)`, "g"), `set${cap}($1)`);
|
||||
}
|
||||
return `() => { ${body} }`;
|
||||
}
|
||||
function renderAttrs2(attrs, states) {
|
||||
return attrs.map((attr) => {
|
||||
if (attr.event) {
|
||||
if (attr.name.startsWith("browser-"))
|
||||
return "";
|
||||
const eventName = attr.name.startsWith("mobile-") ? attr.name.slice(7) : attr.name;
|
||||
const event = eventName === "click" || eventName === "press" ? "onPress" : eventName === "input" || eventName === "change" ? "onChangeText" : `on${eventName[0].toUpperCase()}${eventName.slice(1)}`;
|
||||
return ` ${event}={${eventBody(attr.value, states)}}`;
|
||||
}
|
||||
if (attr.name === "data-native-browser" || attr.name.startsWith("data-native-on-browser-"))
|
||||
return "";
|
||||
if (attr.name === "data-native-options" || attr.name === "data-native-only" || attr.name === "data-native-requires" || attr.name === "data-native-unsupported")
|
||||
return "";
|
||||
if (attr.name === "data-native-mobile") {
|
||||
throw new NativeCompileError(`Declarative native capability "${attr.value}" currently targets browser/Capacitor pages. In Expo output, call the installed Expo package from an @mobile-event handler.`);
|
||||
}
|
||||
const name = attrMap[attr.name] ?? attr.name;
|
||||
if (name === "__href")
|
||||
return ` onPress={() => router.push(${JSON.stringify(attr.value)})}`;
|
||||
if (name === "source") {
|
||||
const expr2 = expression(attr.value);
|
||||
return ` source={${expr2 ? `{ uri: ${expr2} }` : `{ uri: ${JSON.stringify(attr.value)} }`}}`;
|
||||
}
|
||||
if (name === "style" && attr.name !== "style") {
|
||||
return ` style={[${attr.value.split(/\s+/).filter(Boolean).map((value) => `styles[${JSON.stringify(value)}]`).join(", ")} ]}`;
|
||||
}
|
||||
if (name === "style") {
|
||||
const inlineExpression = expression(attr.value);
|
||||
if (inlineExpression)
|
||||
return ` style={${inlineExpression}}`;
|
||||
throw new NativeCompileError('Inline CSS strings are not portable to native; use class="name" and a page style block');
|
||||
}
|
||||
if (attr.boolean)
|
||||
return ` ${name}`;
|
||||
const expr = expression(attr.value);
|
||||
return expr ? ` ${name}={${expr}}` : ` ${name}=${JSON.stringify(attr.value)}`;
|
||||
}).join("");
|
||||
}
|
||||
function renderNode2(node, states, key) {
|
||||
if (node.type === "text")
|
||||
return textJsx(node.value);
|
||||
if (node.type === "each") {
|
||||
const params = node.index ? `${node.item}, ${node.index}` : `${node.item}, __index`;
|
||||
const body = node.body.map((child, index) => renderNode2(child, states, index === 0 ? node.index ?? "__index" : undefined)).join("");
|
||||
const empty = node.empty.map((child) => renderNode2(child, states)).join("");
|
||||
return `{(${node.list})?.length ? (${node.list}).map((${params}) => <>${body}</>) : <>${empty}</>}`;
|
||||
}
|
||||
if (node.type === "if") {
|
||||
const result = node.branches.reduceRight((fallback, branch) => branch.cond === null ? `<>${branch.body.map((child) => renderNode2(child, states)).join("")}</>` : `(${branch.cond}) ? <>${branch.body.map((child) => renderNode2(child, states)).join("")}</> : ${fallback}`, "null");
|
||||
return `{${result}}`;
|
||||
}
|
||||
const nativeOnly = node.attrs.find((attr) => !attr.event && attr.name === "data-native-only")?.value;
|
||||
if (nativeOnly === "browser" || nativeOnly === "web")
|
||||
return "";
|
||||
const nativeTag = tagMap[node.tag.toLowerCase()] ?? (/^[A-Z]/.test(node.tag) ? node.tag : undefined);
|
||||
if (!nativeTag)
|
||||
throw new NativeCompileError(`HTML element <${node.tag}> has no native equivalent`);
|
||||
const attrs = renderAttrs2(node.attrs, states) + (key ? ` key={${key}}` : "");
|
||||
if (nativeTag === "TextInput" || nativeTag === "Image" || nativeTag === "ActivityIndicator")
|
||||
return `<${nativeTag}${attrs} />`;
|
||||
const children = node.children.map((child) => {
|
||||
if (child.type !== "text")
|
||||
return renderNode2(child, states);
|
||||
if (!child.value.trim())
|
||||
return "";
|
||||
const text = textJsx(child.value);
|
||||
return nativeTag === "Text" ? text : `<Text>${text}</Text>`;
|
||||
}).join("");
|
||||
return `<${nativeTag}${attrs}>${children}</${nativeTag}>`;
|
||||
}
|
||||
function nativeStyles(blocks) {
|
||||
const entries = [];
|
||||
for (const block of blocks) {
|
||||
for (const match of block.matchAll(/\.([A-Za-z_][\w-]*)\s*\{([^}]*)\}/g)) {
|
||||
const props = [];
|
||||
for (const declaration of match[2].split(";")) {
|
||||
const colon = declaration.indexOf(":");
|
||||
if (colon < 0)
|
||||
continue;
|
||||
const name = declaration.slice(0, colon).trim().replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
||||
let value = declaration.slice(colon + 1).trim();
|
||||
if (/^-?\d+(?:\.\d+)?px$/.test(value))
|
||||
value = Number(value.slice(0, -2));
|
||||
props.push(`${JSON.stringify(name)}: ${typeof value === "number" ? value : JSON.stringify(value)}`);
|
||||
}
|
||||
entries.push(`${JSON.stringify(match[1])}: { ${props.join(", ")} }`);
|
||||
}
|
||||
}
|
||||
return `const styles = StyleSheet.create({ ${entries.join(`,
|
||||
`)} });`;
|
||||
}
|
||||
function generateNative(ast) {
|
||||
if (ast.kind !== "page")
|
||||
throw new NativeCompileError("Native route compilation currently accepts page files only");
|
||||
if (ast.dataApis.length)
|
||||
throw new NativeCompileError("Data API blocks are not yet portable to native screens; fetch through the generated native backend helper");
|
||||
const states = new Set(ast.states.map((state) => state.name));
|
||||
const hooks = ast.states.map((state) => {
|
||||
const cap = state.name[0].toUpperCase() + state.name.slice(1);
|
||||
return ` const [${state.name}, set${cap}] = useState(${state.expr});`;
|
||||
}).join(`
|
||||
`);
|
||||
const body = ast.view.map((node) => renderNode2(node, states)).join("");
|
||||
return `// generated from .wrn for Expo/React Native
|
||||
import React, { useState } from "react";
|
||||
import { ActivityIndicator, FlatList, Image, Pressable, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View } from "react-native";
|
||||
import { useRouter } from "expo-router";
|
||||
|
||||
export default function ${ast.name}() {
|
||||
const router = useRouter();
|
||||
${hooks}
|
||||
return <>${body}</>;
|
||||
}
|
||||
|
||||
${nativeStyles(ast.styles)}
|
||||
`;
|
||||
}
|
||||
|
||||
// ../../packages/compiler/src/index.ts
|
||||
function compileNativeWireFile(source) {
|
||||
return generateNative(parse(source));
|
||||
}
|
||||
function compileWireFile(source) {
|
||||
const ast = parse(source);
|
||||
return `// compiled from .wrn
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"use strict";
|
||||
|
||||
const vscode = require("vscode");
|
||||
const { formatWrn } = require("./formatter.js");
|
||||
|
||||
// The .wrn compiler, bundled to CJS by `bun run build:compiler`. Loaded
|
||||
// defensively so the rest of the extension (highlighting, snippets, completion)
|
||||
@@ -112,6 +113,17 @@ function activate(context) {
|
||||
|
||||
// Completions.
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerDocumentFormattingEditProvider("wrn", {
|
||||
provideDocumentFormattingEdits(document, options) {
|
||||
if (!vscode.workspace.getConfiguration("wrnexus").get("format.enable", true)) return [];
|
||||
const formatted = formatWrn(document.getText(), options);
|
||||
if (formatted === document.getText()) return [];
|
||||
const end = document.positionAt(document.getText().length);
|
||||
return [
|
||||
vscode.TextEdit.replace(new vscode.Range(new vscode.Position(0, 0), end), formatted),
|
||||
];
|
||||
},
|
||||
}),
|
||||
vscode.languages.registerCompletionItemProvider(
|
||||
"wrn",
|
||||
{ provideCompletionItems: provideCompletions },
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
"use strict";
|
||||
|
||||
const VOID_ELEMENTS = new Set([
|
||||
"area",
|
||||
"base",
|
||||
"br",
|
||||
"col",
|
||||
"embed",
|
||||
"hr",
|
||||
"img",
|
||||
"input",
|
||||
"link",
|
||||
"meta",
|
||||
"param",
|
||||
"source",
|
||||
"track",
|
||||
"wbr",
|
||||
]);
|
||||
|
||||
/** Count structural curly braces while ignoring quoted strings and line comments. */
|
||||
function curlyDelta(line, state) {
|
||||
let delta = 0;
|
||||
let quote = null;
|
||||
let escaped = false;
|
||||
|
||||
for (let index = 0; index < line.length; index++) {
|
||||
const char = line[index];
|
||||
const next = line[index + 1];
|
||||
|
||||
if (state.blockComment) {
|
||||
if (char === "*" && next === "/") {
|
||||
state.blockComment = false;
|
||||
index++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (state.template) {
|
||||
if (!escaped && char === "`") state.template = false;
|
||||
escaped = !escaped && char === "\\";
|
||||
if (char !== "\\") escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (quote) {
|
||||
if (!escaped && char === quote) quote = null;
|
||||
escaped = !escaped && char === "\\";
|
||||
if (char !== "\\") escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (char === "/" && next === "*") {
|
||||
state.blockComment = true;
|
||||
index++;
|
||||
} else if (char === "/" && next === "/") {
|
||||
break;
|
||||
} else if (char === "`") {
|
||||
state.template = true;
|
||||
} else if (char === '"' || char === "'") {
|
||||
quote = char;
|
||||
} else if (char === "{") {
|
||||
delta++;
|
||||
} else if (char === "}") {
|
||||
delta--;
|
||||
}
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
/** Return the net nesting introduced by HTML-like tags on this line. */
|
||||
function htmlDelta(line) {
|
||||
let delta = 0;
|
||||
const tags = line.matchAll(/<\s*(\/?)\s*([A-Za-z][\w:-]*)\b[^>]*>/g);
|
||||
for (const match of tags) {
|
||||
const full = match[0];
|
||||
const closing = match[1] === "/";
|
||||
const name = match[2].toLowerCase();
|
||||
if (closing) delta--;
|
||||
else if (!VOID_ELEMENTS.has(name) && !/\/\s*>$/.test(full)) delta++;
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
function leadingClosers(line) {
|
||||
let count = /^\s*}/.test(line) ? 1 : 0;
|
||||
const tags = line.match(/^\s*((?:<\/\s*[A-Za-z][\w:-]*\s*>\s*)+)/);
|
||||
if (tags) count += [...tags[1].matchAll(/<\//g)].length;
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format WRN source conservatively: normalize structural indentation and
|
||||
* trailing whitespace without rewriting expressions, HTML, CSS, or JS.
|
||||
*/
|
||||
function formatWrn(text, options = {}) {
|
||||
const tabSize = Math.max(1, Number(options.tabSize) || 2);
|
||||
const unit = options.insertSpaces === false ? "\t" : " ".repeat(tabSize);
|
||||
const hadFinalNewline = /\r?\n$/.test(text);
|
||||
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
||||
if (hadFinalNewline) lines.pop();
|
||||
|
||||
let depth = 0;
|
||||
const state = { blockComment: false, template: false };
|
||||
const formatted = lines.map((original) => {
|
||||
const line = original.trimEnd();
|
||||
if (!line.trim()) return "";
|
||||
|
||||
// Preserve multiline template-string content because its whitespace can be data.
|
||||
const wasTemplate = state.template;
|
||||
const trimmed = line.trimStart();
|
||||
const indent = Math.max(0, depth - leadingClosers(trimmed));
|
||||
const output = wasTemplate ? line : unit.repeat(indent) + trimmed;
|
||||
depth = Math.max(0, depth + curlyDelta(trimmed, state) + htmlDelta(trimmed));
|
||||
return output;
|
||||
});
|
||||
|
||||
return formatted.join("\n") + (hadFinalNewline ? "\n" : "");
|
||||
}
|
||||
|
||||
module.exports = { formatWrn };
|
||||
@@ -58,5 +58,19 @@ try {
|
||||
bad("compiler bundle loads", e.message);
|
||||
}
|
||||
|
||||
// 4. Formatter normalizes nested WRN blocks and is idempotent.
|
||||
try {
|
||||
const { formatWrn } = require(join(root, "src/formatter.js"));
|
||||
const source = `page Home {\nview {\n<main>\n<h1>Hello</h1>\n</main>\n}\n}\n`;
|
||||
const expected = `page Home {\n view {\n <main>\n <h1>Hello</h1>\n </main>\n }\n}\n`;
|
||||
const formatted = formatWrn(source, { insertSpaces: true, tabSize: 2 });
|
||||
formatted === expected ? ok("formatter indents WRN blocks") : bad("formatter indents WRN blocks");
|
||||
formatWrn(formatted, { insertSpaces: true, tabSize: 2 }) === formatted
|
||||
? ok("formatter is idempotent")
|
||||
: bad("formatter is idempotent");
|
||||
} catch (e) {
|
||||
bad("formatter loads", e.message);
|
||||
}
|
||||
|
||||
console.log(failures === 0 ? "\nAll extension checks passed." : `\n${failures} check(s) failed.`);
|
||||
process.exit(failures === 0 ? 0 : 1);
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ai",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/authz",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/cli",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -51,7 +51,9 @@ mobile/.expo/
|
||||
|
||||
# Editors and operating systems
|
||||
.idea/
|
||||
.vscode/
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/extensions.json
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
@@ -132,7 +134,14 @@ const tsconfigRootDir = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ["node_modules/**", "dist/**", ".wrnexus/**", "**/.wrnexus/**", "mobile/android/**", "mobile/ios/**"],
|
||||
ignores: [
|
||||
"node_modules/**",
|
||||
"dist/**",
|
||||
".wrnexus/**",
|
||||
"**/.wrnexus/**",
|
||||
"mobile/android/**",
|
||||
"mobile/ios/**",
|
||||
],
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
@@ -187,6 +196,23 @@ indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
`,
|
||||
".vscode/settings.json": `{
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"prettier.requireConfig": true,
|
||||
"[wrn]": {
|
||||
"editor.defaultFormatter": "wrnexus.wrnexus",
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
}
|
||||
`,
|
||||
".vscode/extensions.json": `{
|
||||
"recommendations": ["wrnexus.wrnexus", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
|
||||
}
|
||||
`,
|
||||
"wrnexus.config.ts": `import type { AppConfig } from "@wrnexus/styles";
|
||||
|
||||
@@ -274,7 +300,13 @@ Allow: /
|
||||
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
|
||||
|
||||
body {
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
font-family:
|
||||
ui-sans-serif,
|
||||
system-ui,
|
||||
-apple-system,
|
||||
"Segoe UI",
|
||||
Roboto,
|
||||
sans-serif;
|
||||
}
|
||||
`,
|
||||
"app/pages/index.wrn": `// Home page (route: /). SSR-first: the view is server-rendered, then components
|
||||
|
||||
@@ -172,6 +172,87 @@ const MIGRATIONS: Migration[] = [
|
||||
if (!ctx.dryRun) writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "0.2.19",
|
||||
id: "vscode-formatting",
|
||||
description: "Add safe VS Code format-on-save defaults and extension recommendations",
|
||||
apply(ctx) {
|
||||
const vscode = join(ctx.appRoot, ".vscode");
|
||||
const settings = join(vscode, "settings.json");
|
||||
const extensions = join(vscode, "extensions.json");
|
||||
const prettierIgnore = join(ctx.appRoot, ".prettierignore");
|
||||
const gitignore = join(ctx.appRoot, ".gitignore");
|
||||
|
||||
const files: Array<[string, string, string]> = [
|
||||
[
|
||||
settings,
|
||||
JSON.stringify(
|
||||
{
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" },
|
||||
"prettier.requireConfig": true,
|
||||
"[wrn]": {
|
||||
"editor.defaultFormatter": "wrnexus.wrnexus",
|
||||
"editor.formatOnSave": true,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
) + "\n",
|
||||
".vscode/settings.json",
|
||||
],
|
||||
[
|
||||
extensions,
|
||||
JSON.stringify(
|
||||
{
|
||||
recommendations: [
|
||||
"wrnexus.wrnexus",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
) + "\n",
|
||||
".vscode/extensions.json",
|
||||
],
|
||||
];
|
||||
|
||||
for (const [file, contents, label] of files) {
|
||||
if (existsSync(file)) continue;
|
||||
ctx.log(`+ ${label}`);
|
||||
if (!ctx.dryRun) {
|
||||
mkdirSync(vscode, { recursive: true });
|
||||
writeFileSync(file, contents, "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
const prettier = existsSync(prettierIgnore) ? readFileSync(prettierIgnore, "utf8") : "";
|
||||
if (!new Set(prettier.split(/\r?\n/).map((line) => line.trim())).has("CLAUDE.md")) {
|
||||
ctx.log("+ .prettierignore: CLAUDE.md");
|
||||
if (!ctx.dryRun) {
|
||||
writeFileSync(prettierIgnore, `${prettier.replace(/\s*$/, "")}\nCLAUDE.md\n`, "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
if (existsSync(gitignore)) {
|
||||
const current = readFileSync(gitignore, "utf8");
|
||||
const lines = current.split(/\r?\n/);
|
||||
const vscodeIgnore = lines.findIndex((line) => line.trim() === ".vscode/");
|
||||
const needed = [".vscode/*", "!.vscode/settings.json", "!.vscode/extensions.json"];
|
||||
if (vscodeIgnore >= 0 || needed.some((entry) => !lines.includes(entry))) {
|
||||
ctx.log("~ .gitignore: track shared VS Code configuration");
|
||||
if (!ctx.dryRun) {
|
||||
const filtered = lines.filter((line) => line.trim() !== ".vscode/");
|
||||
const have = new Set(filtered.map((line) => line.trim()));
|
||||
for (const entry of needed) if (!have.has(entry)) filtered.push(entry);
|
||||
writeFileSync(gitignore, filtered.join("\n").replace(/\n*$/, "\n"), "utf8");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** Release tooling uses this to require an explicit migration entry per version. */
|
||||
@@ -246,6 +327,9 @@ function backupProjectFiles(appRoot: string, from: string, target: string): stri
|
||||
"wrnexus.config.mjs",
|
||||
"tsconfig.json",
|
||||
".gitignore",
|
||||
".prettierignore",
|
||||
".vscode/settings.json",
|
||||
".vscode/extensions.json",
|
||||
"CLAUDE.md",
|
||||
"public/llms.txt",
|
||||
]) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { expect, test } from "bun:test";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { format } from "prettier";
|
||||
import { scaffoldApp } from "../src/create.ts";
|
||||
import { currentCliVersion } from "../src/update-notifier.ts";
|
||||
|
||||
@@ -69,6 +70,15 @@ test("scaffoldApp pins the current framework and exposes llms.txt publicly", ()
|
||||
expect(existsSync(join(root, "public", "llms.txt"))).toBe(true);
|
||||
expect(existsSync(join(root, "llms.txt"))).toBe(false);
|
||||
expect(readFileSync(join(root, ".prettierignore"), "utf8")).toContain("CLAUDE.md");
|
||||
expect(readFileSync(join(root, ".vscode", "settings.json"), "utf8")).toContain(
|
||||
'"editor.formatOnSave": true',
|
||||
);
|
||||
expect(readFileSync(join(root, ".vscode", "settings.json"), "utf8")).toContain(
|
||||
'"editor.defaultFormatter": "wrnexus.wrnexus"',
|
||||
);
|
||||
expect(readFileSync(join(root, ".vscode", "extensions.json"), "utf8")).toContain(
|
||||
"wrnexus.wrnexus",
|
||||
);
|
||||
expect(readFileSync(join(root, "app", "pages", "index.wrn"), "utf8")).toContain(
|
||||
'href="/api/hello"',
|
||||
);
|
||||
@@ -77,3 +87,34 @@ test("scaffoldApp pins the current framework and exposes llms.txt publicly", ()
|
||||
rmSync(parent, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("scaffoldApp emits Prettier-compliant editor and framework files", async () => {
|
||||
const parent = mkdtempSync(join(tmpdir(), "wrnexus-create-"));
|
||||
const root = join(parent, "formatted-app");
|
||||
|
||||
try {
|
||||
scaffoldApp(root, "formatted-app");
|
||||
for (const relative of [
|
||||
"app/styles/global.css",
|
||||
"eslint.config.js",
|
||||
".vscode/settings.json",
|
||||
".vscode/extensions.json",
|
||||
]) {
|
||||
const file = join(root, relative);
|
||||
const source = readFileSync(file, "utf8");
|
||||
const formatted = await format(source, {
|
||||
filepath: file,
|
||||
printWidth: 100,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
semi: true,
|
||||
singleQuote: false,
|
||||
trailingComma: "all",
|
||||
endOfLine: "lf",
|
||||
});
|
||||
expect(formatted).toBe(source);
|
||||
}
|
||||
} finally {
|
||||
rmSync(parent, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -61,3 +61,35 @@ test("0.2.18 migration adds helpers to existing apps and is idempotent", () => {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("0.2.19 migration adds VS Code formatting defaults without overwriting settings", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-update-"));
|
||||
mkdirSync(join(root, "app", "pages"), { recursive: true });
|
||||
mkdirSync(join(root, ".vscode"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(root, "package.json"),
|
||||
JSON.stringify({ name: "editor-app", wrnexus: { version: "0.2.18" } }),
|
||||
);
|
||||
writeFileSync(join(root, ".gitignore"), ".vscode/\n");
|
||||
writeFileSync(join(root, ".prettierignore"), "node_modules/\n");
|
||||
writeFileSync(join(root, ".vscode", "settings.json"), '{"editor.tabSize":4}\n');
|
||||
|
||||
try {
|
||||
updateApp(root, "0.2.19", false);
|
||||
updateApp(root, "0.2.19", false);
|
||||
|
||||
expect(readFileSync(join(root, ".vscode", "settings.json"), "utf8")).toBe(
|
||||
'{"editor.tabSize":4}\n',
|
||||
);
|
||||
expect(readFileSync(join(root, ".vscode", "extensions.json"), "utf8")).toContain(
|
||||
"wrnexus.wrnexus",
|
||||
);
|
||||
expect(readFileSync(join(root, ".prettierignore"), "utf8")).toBe("node_modules/\nCLAUDE.md\n");
|
||||
const gitignore = readFileSync(join(root, ".gitignore"), "utf8");
|
||||
expect(gitignore).not.toContain(".vscode/\n");
|
||||
expect(gitignore).toContain("!.vscode/settings.json");
|
||||
expect(gitignore).toContain("!.vscode/extensions.json");
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/compiler",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/core",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/csr",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/db",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -142,7 +142,7 @@ interface ProdOptions {
|
||||
|
||||
### `startGateway(opts)` — multi-app gateway
|
||||
|
||||
Serves several apps behind one port and routes each request to the right app by its `Host` header. Each app runs as its own child process (full isolation); the gateway is a thin host-based reverse proxy for HTTP and WebSocket. Apps communicate at runtime via `@wrnexus/pubsub` (use the Redis driver so messages cross processes).
|
||||
Serves several apps behind one port and routes each request to the right app by its `Host` header. Each app runs as its own child process (full isolation); the gateway is a thin host-based reverse proxy for HTTP and WebSocket. In development, the gateway supervises every child: editing a page, component, API, or middleware restarts only that app and the HMR client reconnects to display the latest page. Apps communicate at runtime via `@wrnexus/pubsub` (use the Redis driver so messages cross processes).
|
||||
|
||||
```ts
|
||||
interface GatewayOptions {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-server",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { RESTART_EXIT_CODE } from "./restart.ts";
|
||||
|
||||
/** Per-app access control, enforced at the gateway before proxying. */
|
||||
export interface GatewayAuth {
|
||||
@@ -71,7 +72,7 @@ export interface RunningGateway {
|
||||
interface Target extends GatewayApp {
|
||||
port: number;
|
||||
origin: string;
|
||||
child: ChildProcess;
|
||||
child?: ChildProcess;
|
||||
}
|
||||
|
||||
interface WsBridge {
|
||||
@@ -81,6 +82,18 @@ interface WsBridge {
|
||||
queue: (string | ArrayBufferLike | ArrayBufferView)[];
|
||||
}
|
||||
|
||||
/** Decide whether a gateway child should be relaunched after it exits. */
|
||||
export function gatewayRestartDelay(
|
||||
mode: "development" | "production",
|
||||
code: number | null,
|
||||
signal: NodeJS.Signals | null,
|
||||
): number | null {
|
||||
if (mode !== "development" || signal) return null;
|
||||
if (code === RESTART_EXIT_CODE) return 0;
|
||||
if (code && code !== 0) return 1200;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Fixed-window rate limiter keyed by client IP. */
|
||||
function makeRateLimiter(max: number, windowMs: number) {
|
||||
const hits = new Map<string, { count: number; reset: number }>();
|
||||
@@ -196,9 +209,9 @@ function applyEdgeHeaders(res: Response): Response {
|
||||
async function waitReady(target: Target, timeoutMs = 15000): Promise<void> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
for (;;) {
|
||||
if (target.child.exitCode !== null) {
|
||||
if (!target.child || target.child.exitCode !== null) {
|
||||
throw new Error(
|
||||
`app '${target.name}' exited with code ${target.child.exitCode} during startup`,
|
||||
`app '${target.name}' exited with code ${target.child?.exitCode ?? "unknown"} during startup`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
@@ -245,28 +258,52 @@ export async function startGateway(opts: GatewayOptions): Promise<RunningGateway
|
||||
const displayHost = hostname === "0.0.0.0" || hostname === "::" ? "localhost" : hostname;
|
||||
const serveEntry = fileURLToPath(import.meta.resolve("@wrnexus/dev-server/serve-entry"));
|
||||
|
||||
let stopping = false;
|
||||
const targets: Target[] = opts.apps.map((app, i) => {
|
||||
const appPort = app.port ?? port + 1 + i;
|
||||
const dir = resolve(app.dir);
|
||||
const child =
|
||||
mode === "production"
|
||||
? spawn(process.execPath, [join(dir, "dist", "server.js")], {
|
||||
stdio: "inherit",
|
||||
env: { ...process.env, PORT: String(appPort) },
|
||||
})
|
||||
: spawn(
|
||||
process.execPath,
|
||||
[serveEntry, join(dir, "app"), String(appPort), mode, "127.0.0.1"],
|
||||
{
|
||||
const target: Target = {
|
||||
...app,
|
||||
port: appPort,
|
||||
origin: `http://127.0.0.1:${appPort}`,
|
||||
};
|
||||
|
||||
const launch = (): void => {
|
||||
if (stopping) return;
|
||||
const child =
|
||||
mode === "production"
|
||||
? spawn(process.execPath, [join(dir, "dist", "server.js")], {
|
||||
stdio: "inherit",
|
||||
},
|
||||
);
|
||||
return { ...app, port: appPort, origin: `http://127.0.0.1:${appPort}`, child };
|
||||
env: { ...process.env, PORT: String(appPort) },
|
||||
})
|
||||
: spawn(
|
||||
process.execPath,
|
||||
[serveEntry, join(dir, "app"), String(appPort), mode, "127.0.0.1"],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
target.child = child;
|
||||
|
||||
child.once("exit", (code, signal) => {
|
||||
if (stopping || target.child !== child) return;
|
||||
const delay = gatewayRestartDelay(mode, code, signal);
|
||||
if (delay === null) return;
|
||||
if (delay > 0) {
|
||||
console.error(`[wrnexus] app '${target.name}' exited (code ${code}); retrying in 1.2s…`);
|
||||
setTimeout(launch, delay);
|
||||
} else {
|
||||
launch();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
launch();
|
||||
return target;
|
||||
});
|
||||
|
||||
const stopChildren = () => {
|
||||
stopping = true;
|
||||
for (const target of targets) {
|
||||
if (target.child.exitCode === null) target.child.kill();
|
||||
if (target.child?.exitCode === null) target.child.kill();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -28,9 +28,9 @@ import { createHandlers, type WsData } from "./runtime.ts";
|
||||
import { createDevAssetServer } from "./assets.ts";
|
||||
import { HmrHub } from "./hmr.ts";
|
||||
import { startWatcher } from "./watch.ts";
|
||||
import { RESTART_EXIT_CODE } from "./restart.ts";
|
||||
|
||||
/** Exit code the child uses to ask the dev supervisor for a fresh process. */
|
||||
export const RESTART_EXIT_CODE = 97;
|
||||
export { RESTART_EXIT_CODE } from "./restart.ts";
|
||||
|
||||
export interface ServeOptions {
|
||||
appDir: string;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/** Exit code a dev-server child uses to request a clean supervisor restart. */
|
||||
export const RESTART_EXIT_CODE = 97;
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
forwardAuthFailure,
|
||||
forwardAuthHeaders,
|
||||
gatewayProxyHeaders,
|
||||
gatewayRestartDelay,
|
||||
} from "../src/gateway.ts";
|
||||
|
||||
test("gateway uses platform-safe hostname defaults", () => {
|
||||
@@ -56,3 +57,11 @@ test("forward auth describes the original gateway request", () => {
|
||||
expect(headers.get("cookie")).toBe("session=abc");
|
||||
expect(headers.get("authorization")).toBe("Bearer token");
|
||||
});
|
||||
|
||||
test("gateway respawns development apps after an HMR restart exit", () => {
|
||||
expect(gatewayRestartDelay("development", 97, null)).toBe(0);
|
||||
expect(gatewayRestartDelay("development", 1, null)).toBe(1200);
|
||||
expect(gatewayRestartDelay("development", 0, null)).toBeNull();
|
||||
expect(gatewayRestartDelay("development", 97, "SIGTERM")).toBeNull();
|
||||
expect(gatewayRestartDelay("production", 97, null)).toBeNull();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/encryption",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/helpers",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/i18n",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/jwt",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/mobile",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/native",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/oauth",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/pubsub",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/queue",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/reactive",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/router",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ssr",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/styles",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/test",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/tracking",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ui",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/uploader",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/validation",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
Reference in New Issue
Block a user