refactor: migrate legacy wire namespace to wrn
This commit is contained in:
@@ -31,7 +31,7 @@ consumers. A generated `wrnexus.contracts.json` can be used instead of a module.
|
||||
|
||||
## Overview
|
||||
|
||||
Define a schema once with the fluent `v` builder, then reuse it in three places: `.parse()` runs server-side and returns coerced values plus per-field errors; `.describe()` emits a plain-JSON `SchemaDescriptor` that the browser runtime interprets (no `eval`, no bundled validator); and helpers like `parseBody` and `parseEnv` wire schemas straight into API routes and startup config. The server rule logic (`applyRule`/`checkField`) and the client runtime (`VALIDATE_RUNTIME`) mirror each other exactly, so a form validates identically in both places. Schemas are conventionally kept in `app/schemas/`.
|
||||
Define a schema once with the fluent `v` builder, then reuse it in three places: `.parse()` runs server-side and returns coerced values plus per-field errors; `.describe()` emits a plain-JSON `SchemaDescriptor` that the browser runtime interprets (no `eval`, no bundled validator); and helpers like `parseBody` and `parseEnv` wrn schemas straight into API routes and startup config. The server rule logic (`applyRule`/`checkField`) and the client runtime (`VALIDATE_RUNTIME`) mirror each other exactly, so a form validates identically in both places. Schemas are conventionally kept in `app/schemas/`.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -136,8 +136,8 @@ renderSchemasScript(descriptors: Record<string, SchemaDescriptor>): string
|
||||
VALIDATE_RUNTIME: string
|
||||
```
|
||||
|
||||
- `renderSchemasScript` produces `window.__wireSchemas = { name: descriptor, … };` to inline in the page.
|
||||
- `VALIDATE_RUNTIME` is a self-contained, eval-free IIFE string. Injected as a `<script>`, it binds every `form[data-schema]` and validates on submit and blur, writing messages into `[data-error="<field>"]` elements and toggling `aria-invalid` / `.wire-invalid`. On a valid submit it `fetch`es the form `action` as JSON (attaching the `wire-csrf` cookie as an `x-csrf-token` header), then follows `data-redirect` / a `redirect` in the response, surfaces server-side field errors, and fires `wire:success` / `wire:error` events. It exposes `window.__wireValidate.init(root)` and self-initializes on `DOMContentLoaded`.
|
||||
- `renderSchemasScript` produces `window.__wrnSchemas = { name: descriptor, … };` to inline in the page.
|
||||
- `VALIDATE_RUNTIME` is a self-contained, eval-free IIFE string. Injected as a `<script>`, it binds every `form[data-schema]` and validates on submit and blur, writing messages into `[data-error="<field>"]` elements and toggling `aria-invalid` / `.wrn-invalid`. On a valid submit it `fetch`es the form `action` as JSON (attaching the `wrn-csrf` cookie as an `x-csrf-token` header), then follows `data-redirect` / a `redirect` in the response, surfaces server-side field errors, and fires `wrn:success` / `wrn:error` events. It exposes `window.__wrnValidate.init(root)` and self-initializes on `DOMContentLoaded`.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -186,7 +186,7 @@ export const env = parseEnv(
|
||||
// throws one readable error listing every bad variable if misconfigured
|
||||
```
|
||||
|
||||
Wire the same schema into the browser:
|
||||
WrNexus the same schema into the browser:
|
||||
|
||||
```ts
|
||||
import { renderSchemasScript, VALIDATE_RUNTIME } from "@wrnexus/validation";
|
||||
|
||||
@@ -7,6 +7,6 @@ component FieldError {
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<p {...attrs} id='{field ? field + "-error" : ""}' data-error='{field}' role="alert" hidden='{!message}' class='m-0 min-h-4 text-xs text-[var(--wire-color-danger)] {class}'>{message}</p>
|
||||
<p {...attrs} id='{field ? field + "-error" : ""}' data-error='{field}' role="alert" hidden='{!message}' class='m-0 min-h-4 text-xs text-[var(--wrn-color-danger)] {class}'>{message}</p>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/validation",
|
||||
"version": "0.8.8",
|
||||
"version": "0.8.9",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Client-side validation. `renderSchemasScript` bakes the discovered schema
|
||||
* descriptors into `window.__wireSchemas`; `VALIDATE_RUNTIME` is a generic,
|
||||
* descriptors into `window.__wrnSchemas`; `VALIDATE_RUNTIME` is a generic,
|
||||
* eval-free validator that reads them and validates every `form[data-schema]`
|
||||
* on submit and blur, writing messages into `[data-error="<field>"]` elements.
|
||||
* The rule logic mirrors `checkField`/`applyRule` in index.ts.
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
import type { SchemaDescriptor } from "./index.ts";
|
||||
|
||||
/** `window.__wireSchemas = { name: descriptor, ... }` for the client validator. */
|
||||
/** `window.__wrnSchemas = { name: descriptor, ... }` for the client validator. */
|
||||
export function renderSchemasScript(descriptors: Record<string, SchemaDescriptor>): string {
|
||||
return `window.__wireSchemas=Object.assign(window.__wireSchemas||{},${JSON.stringify(descriptors)});`;
|
||||
return `window.__wrnSchemas=Object.assign(window.__wrnSchemas||{},${JSON.stringify(descriptors)});`;
|
||||
}
|
||||
|
||||
export const VALIDATE_RUNTIME = String.raw`
|
||||
@@ -72,9 +72,9 @@ export const VALIDATE_RUNTIME = String.raw`
|
||||
if (box) box.textContent = err || "";
|
||||
var el = form.elements[name];
|
||||
if (el && el.setAttribute) {
|
||||
if (err) { el.setAttribute("aria-invalid", "true"); if (el.classList) el.classList.add("wire-invalid"); }
|
||||
else { el.removeAttribute("aria-invalid"); if (el.classList) el.classList.remove("wire-invalid"); }
|
||||
var field = el.closest && el.closest(".wire-next--field, .wire-next--choice-field");
|
||||
if (err) { el.setAttribute("aria-invalid", "true"); if (el.classList) el.classList.add("wrn-invalid"); }
|
||||
else { el.removeAttribute("aria-invalid"); if (el.classList) el.classList.remove("wrn-invalid"); }
|
||||
var field = el.closest && el.closest(".wrn-next--field, .wrn-next--choice-field");
|
||||
if (field) field.setAttribute("data-invalid", err ? "true" : "false");
|
||||
}
|
||||
}
|
||||
@@ -124,7 +124,7 @@ export const VALIDATE_RUNTIME = String.raw`
|
||||
|
||||
function onSuccess(form, data, responseRedirect) {
|
||||
showFormError(form, "");
|
||||
form.dispatchEvent(new CustomEvent("wire:success", { detail: data, bubbles: true }));
|
||||
form.dispatchEvent(new CustomEvent("wrn:success", { detail: data, bubbles: true }));
|
||||
var redirect = responseRedirect || form.getAttribute("data-redirect") || (data && data.redirect);
|
||||
if (redirect) {
|
||||
// Prefer client-side navigation for same-origin application redirects.
|
||||
@@ -148,7 +148,7 @@ export const VALIDATE_RUNTIME = String.raw`
|
||||
}
|
||||
|
||||
function csrfHeader() {
|
||||
var m = document.cookie.match(/(?:^|;\s*)wire-csrf=([^;]+)/);
|
||||
var m = document.cookie.match(/(?:^|;\s*)wrn-csrf=([^;]+)/);
|
||||
return m ? { "x-csrf-token": decodeURIComponent(m[1]) } : {};
|
||||
}
|
||||
|
||||
@@ -184,18 +184,18 @@ export const VALIDATE_RUNTIME = String.raw`
|
||||
if (errors) Object.keys(errors).forEach(function (f) { showError(form, f, errors[f]); });
|
||||
var message = r.data && (r.data.message || r.data.error);
|
||||
showFormError(form, message || (errors ? "" : ("Request failed (" + r.res.status + ")")));
|
||||
form.dispatchEvent(new CustomEvent("wire:error", { detail: r.data, bubbles: true }));
|
||||
form.dispatchEvent(new CustomEvent("wrn:error", { detail: r.data, bubbles: true }));
|
||||
})
|
||||
.catch(function (error) {
|
||||
var detail = { network: true, message: error && error.message ? error.message : "Network request failed" };
|
||||
showFormError(form, detail.message);
|
||||
form.dispatchEvent(new CustomEvent("wire:error", { detail: detail, bubbles: true }));
|
||||
form.dispatchEvent(new CustomEvent("wrn:error", { detail: detail, bubbles: true }));
|
||||
})
|
||||
.then(function () { btns.forEach(function (b) { b.disabled = false; }); });
|
||||
}
|
||||
|
||||
function runSubmitGuards(form, event) {
|
||||
var guards = form.__wireSubmitGuards || [];
|
||||
var guards = form.__wrnSubmitGuards || [];
|
||||
for (var index = 0; index < guards.length; index += 1) {
|
||||
if (guards[index](event) === false) return false;
|
||||
}
|
||||
@@ -203,21 +203,21 @@ export const VALIDATE_RUNTIME = String.raw`
|
||||
}
|
||||
|
||||
function bind(form) {
|
||||
if (form.__wireValidateBound) return;
|
||||
if (form.__wrnValidateBound) return;
|
||||
var name = form.getAttribute("data-schema");
|
||||
var schema = (window.__wireSchemas || {})[name];
|
||||
var schema = (window.__wrnSchemas || {})[name];
|
||||
|
||||
// Package runtimes may register schemas later in the same page load.
|
||||
// Do not report an error during the first scan.
|
||||
if (!schema) {
|
||||
form.__wireValidatePending = 1;
|
||||
form.__wrnValidatePending = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
form.__wireValidatePending = 0;
|
||||
form.__wireValidateMissingWarned = 0;
|
||||
form.__wireValidateBound = 1;
|
||||
form.__wireValidateMissingWarned = 0;
|
||||
form.__wrnValidatePending = 0;
|
||||
form.__wrnValidateMissingWarned = 0;
|
||||
form.__wrnValidateBound = 1;
|
||||
form.__wrnValidateMissingWarned = 0;
|
||||
// Schema-backed forms use WRNexus messages instead of the browser's
|
||||
// non-themeable native validation bubbles.
|
||||
form.noValidate = true;
|
||||
@@ -242,12 +242,12 @@ export const VALIDATE_RUNTIME = String.raw`
|
||||
}
|
||||
|
||||
function registerSchemas(descriptors, root) {
|
||||
window.__wireSchemas =
|
||||
window.__wireSchemas || {};
|
||||
window.__wrnSchemas =
|
||||
window.__wrnSchemas || {};
|
||||
|
||||
Object.keys(descriptors || {}).forEach(
|
||||
function (name) {
|
||||
window.__wireSchemas[name] =
|
||||
window.__wrnSchemas[name] =
|
||||
descriptors[name];
|
||||
}
|
||||
);
|
||||
@@ -259,7 +259,7 @@ export const VALIDATE_RUNTIME = String.raw`
|
||||
(root || document).querySelectorAll("form[data-schema]").forEach(bind);
|
||||
}
|
||||
|
||||
window.__wireValidate = {
|
||||
window.__wrnValidate = {
|
||||
init: init,
|
||||
registerSchemas: registerSchemas
|
||||
};
|
||||
@@ -279,7 +279,7 @@ export const VALIDATE_RUNTIME = String.raw`
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.__wireValidateBound) {
|
||||
if (form.__wrnValidateBound) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ export const VALIDATE_RUNTIME = String.raw`
|
||||
form.getAttribute("data-schema");
|
||||
|
||||
var schema =
|
||||
(window.__wireSchemas || {})[name];
|
||||
(window.__wrnSchemas || {})[name];
|
||||
|
||||
if (schema) {
|
||||
bind(form);
|
||||
@@ -302,11 +302,11 @@ export const VALIDATE_RUNTIME = String.raw`
|
||||
);
|
||||
|
||||
if (
|
||||
!form.__wireValidateMissingWarned &&
|
||||
!form.__wrnValidateMissingWarned &&
|
||||
window.console &&
|
||||
console.error
|
||||
) {
|
||||
form.__wireValidateMissingWarned = 1;
|
||||
form.__wrnValidateMissingWarned = 1;
|
||||
|
||||
console.error(
|
||||
"[wrnexus:validation] Schema '" +
|
||||
|
||||
@@ -74,13 +74,13 @@ test("browser validation displays the serialized custom required message", () =>
|
||||
<input name="email">
|
||||
<span data-error="email"></span>
|
||||
</form>`;
|
||||
win.__wireSchemas = { login: schema.describe() };
|
||||
win.__wrnSchemas = { login: schema.describe() };
|
||||
(globalThis as Record<string, unknown>).window = win;
|
||||
(globalThis as Record<string, unknown>).document = win.document;
|
||||
|
||||
try {
|
||||
(0, eval)(VALIDATE_RUNTIME);
|
||||
const runtime = win.__wireValidate as { init(root: Document): void };
|
||||
const runtime = win.__wrnValidate as { init(root: Document): void };
|
||||
runtime.init(win.document as unknown as Document);
|
||||
const form = win.document.querySelector("form")!;
|
||||
win.document.querySelector("input")!.dispatchEvent(windowEvent(win, "blur", { bubbles: true }));
|
||||
@@ -219,12 +219,12 @@ test("parseEnv throws one readable error listing every missing/invalid var", ()
|
||||
});
|
||||
|
||||
test("parseEnv defaults to reading the ambient environment", () => {
|
||||
process.env.WIRE_TEST_ENV_VAR = "present";
|
||||
const env = parseEnv<{ WIRE_TEST_ENV_VAR: string }>(
|
||||
v.object({ WIRE_TEST_ENV_VAR: v.string().min(1) }),
|
||||
process.env.WRN_TEST_ENV_VAR = "present";
|
||||
const env = parseEnv<{ WRN_TEST_ENV_VAR: string }>(
|
||||
v.object({ WRN_TEST_ENV_VAR: v.string().min(1) }),
|
||||
);
|
||||
expect(env.WIRE_TEST_ENV_VAR).toBe("present");
|
||||
delete process.env.WIRE_TEST_ENV_VAR;
|
||||
expect(env.WRN_TEST_ENV_VAR).toBe("present");
|
||||
delete process.env.WRN_TEST_ENV_VAR;
|
||||
});
|
||||
|
||||
test("object schemas can be extended without mutating package defaults", () => {
|
||||
@@ -255,15 +255,15 @@ test("validation can bind after a package registers a missing schema", () => {
|
||||
<input name="identifier">
|
||||
<span data-error="identifier"></span>
|
||||
</form>`;
|
||||
win.__wireSchemas = {};
|
||||
win.__wrnSchemas = {};
|
||||
(globalThis as Record<string, unknown>).window = win;
|
||||
(globalThis as Record<string, unknown>).document = win.document;
|
||||
|
||||
try {
|
||||
(0, eval)(VALIDATE_RUNTIME);
|
||||
const runtime = win.__wireValidate as { init(root: Document): void };
|
||||
const runtime = win.__wrnValidate as { init(root: Document): void };
|
||||
runtime.init(win.document as unknown as Document);
|
||||
win.__wireSchemas = {
|
||||
win.__wrnSchemas = {
|
||||
"late-schema": v
|
||||
.object({ identifier: v.string().required("Enter an identifier") })
|
||||
.describe(),
|
||||
@@ -285,12 +285,12 @@ test("schema-backed forms validate on input, change, and blur and synchronize fi
|
||||
const win = new Window() as unknown as Window & Record<string, unknown>;
|
||||
win.document.body.innerHTML = `
|
||||
<form data-schema="profile">
|
||||
<div class="wire-next--field" data-invalid="false">
|
||||
<div class="wrn-next--field" data-invalid="false">
|
||||
<input name="email">
|
||||
<span data-error="email"></span>
|
||||
</div>
|
||||
</form>`;
|
||||
win.__wireSchemas = {
|
||||
win.__wrnSchemas = {
|
||||
profile: v
|
||||
.object({ email: v.string().required("Enter an email").email("Use a valid email") })
|
||||
.describe(),
|
||||
@@ -301,7 +301,7 @@ test("schema-backed forms validate on input, change, and blur and synchronize fi
|
||||
try {
|
||||
(0, eval)(VALIDATE_RUNTIME);
|
||||
const input = win.document.querySelector("input") as HappyDOMHTMLInputElement;
|
||||
const field = win.document.querySelector(".wire-next--field") as HappyDOMHTMLElement;
|
||||
const field = win.document.querySelector(".wrn-next--field") as HappyDOMHTMLElement;
|
||||
const error = win.document.querySelector("[data-error=email]") as HappyDOMHTMLElement;
|
||||
|
||||
input.dispatchEvent(windowEvent(win, "input", { bubbles: true }));
|
||||
@@ -327,7 +327,7 @@ test("submit guards run only after schema fields are valid", () => {
|
||||
<input name="email">
|
||||
<span data-error="email"></span>
|
||||
</form>`;
|
||||
win.__wireSchemas = {
|
||||
win.__wrnSchemas = {
|
||||
guarded: v.object({ email: v.string().required("Enter an email") }).describe(),
|
||||
};
|
||||
(globalThis as Record<string, unknown>).window = win;
|
||||
@@ -336,10 +336,10 @@ test("submit guards run only after schema fields are valid", () => {
|
||||
try {
|
||||
(0, eval)(VALIDATE_RUNTIME);
|
||||
const form = win.document.querySelector("form")! as HappyDOMHTMLFormElement & {
|
||||
__wireSubmitGuards?: Array<() => boolean>;
|
||||
__wrnSubmitGuards?: Array<() => boolean>;
|
||||
};
|
||||
let guardCalls = 0;
|
||||
form.__wireSubmitGuards = [
|
||||
form.__wrnSubmitGuards = [
|
||||
() => {
|
||||
guardCalls += 1;
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user