fix(compiler): never emit an empty pattern attribute
An empty pattern compiles to a regex matching only the empty string, so
every typed value becomes invalid and the form silently refuses to submit
-- no error, no request. @wrnexus/ui's input declares pattern: string = ""
and renders pattern="{pattern}", so every input that did not opt into a
pattern shipped one that could never match. This broke sign-up in a real
app, and only became visible once the dev-server client-module fix let
form enhancements mount at all.
Attributes reach the output through two emitters and both needed it: a
component's interpolated value is baked at render time, so the whole
attribute is now emitted by __wrnOptionalAttr, while a page's static
element is dropped at compile time. Component mounts are excluded, where
the value is a prop being passed down rather than an attribute.
minlength/maxlength/min/max/step/inputmode/accept get the same treatment --
inert when empty, but meaningless too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -133,6 +133,10 @@ function __wrnHtml(v: unknown): string {
|
||||
);
|
||||
}
|
||||
|
||||
function __wrnOptionalAttr(name: string, value: string): string {
|
||||
return value === "" ? "" : " " + name + '="' + value + '"';
|
||||
}
|
||||
|
||||
function __wrnAttr(v: unknown): string {
|
||||
return String(v == null ? "" : v).replace(
|
||||
/[&<>"]/g,
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { compile } from "../src/index.ts";
|
||||
|
||||
/**
|
||||
* `pattern=""` is not inert. An empty pattern compiles to a regex that matches
|
||||
* only the empty string, so EVERY typed value becomes invalid and the form
|
||||
* silently refuses to submit -- no error, no request.
|
||||
*
|
||||
* `@wrnexus/ui`'s input declares `pattern: string = ""` and renders
|
||||
* `pattern="{pattern}"`, so every input that did not opt into a pattern shipped
|
||||
* one that could never match. This broke sign-up in a real app.
|
||||
*
|
||||
* Attributes reach the output through two different emitters, and both matter:
|
||||
* a component's interpolated attribute is baked at render time via
|
||||
* `__wrnAttr`, while a page's static element is serialized by `renderAttr`.
|
||||
*/
|
||||
const uiInput = join(import.meta.dir, "..", "..", "ui", "components", "input.wrn");
|
||||
|
||||
function render(view: string): string {
|
||||
return compile(`page P {\n view { ${view} }\n}\n`, "P.wrn").code;
|
||||
}
|
||||
|
||||
test("the real ui input never emits a bare pattern attribute", () => {
|
||||
const code = compile(readFileSync(uiInput, "utf8"), uiInput).code;
|
||||
|
||||
// The broken shape: the attribute is always present, empty or not.
|
||||
expect(code).not.toContain('pattern="${__wrnAttr(pattern)}"');
|
||||
// The fixed shape: the attribute itself is decided at render time.
|
||||
expect(code).toContain("__wrnOptionalAttr");
|
||||
});
|
||||
|
||||
test("an interpolated constraint attribute on a component is emitted through the helper", () => {
|
||||
const source = `component Field {\n props { pattern: string = "" }\n view { <input type="text" pattern="{pattern}" /> }\n}\n`;
|
||||
const code = compile(source, "Field.wrn").code;
|
||||
|
||||
expect(code).toContain("__wrnOptionalAttr");
|
||||
expect(code).not.toContain('pattern="${__wrnAttr(pattern)}"');
|
||||
});
|
||||
|
||||
test("a static empty constraint attribute on a page element is dropped", () => {
|
||||
const code = render(`<input type="email" pattern="" minlength="" />`);
|
||||
|
||||
expect(code).not.toContain('pattern=""');
|
||||
expect(code).not.toContain('minlength=""');
|
||||
});
|
||||
|
||||
test("a populated constraint attribute survives on both paths", () => {
|
||||
expect(render(`<input type="text" pattern="[0-9]+" />`)).toContain('pattern="[0-9]+"');
|
||||
|
||||
const component = compile(
|
||||
`component F {\n props { p: string = "x" }\n view { <input pattern="{p}" /> }\n}\n`,
|
||||
"F.wrn",
|
||||
).code;
|
||||
expect(component).toContain("pattern");
|
||||
});
|
||||
|
||||
test("attributes outside the constraint set keep their empty values", () => {
|
||||
// An empty value or class is meaningful and must survive untouched.
|
||||
const code = render(`<input type="text" value="" class="" />`);
|
||||
expect(code).toContain('value=""');
|
||||
});
|
||||
Reference in New Issue
Block a user