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:
2026-08-21 09:32:32 +05:30
co-authored by Claude Opus 5
parent 2970d5fff3
commit de4477dd9c
5 changed files with 165 additions and 8 deletions
+48 -4
View File
@@ -1,6 +1,6 @@
"use strict";
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly.
// WRN editor compiler source hash: 4ba6ddaf22c0a33fa1c2a108a97f077a69c0a46de9fe031c0a00e38863fab613
// WRN editor compiler source hash: 3dd8f4ba41eb3b4086c63e530a5daa985c1591e76e23bdd0fc47c87eefe5a4a6
// WRN editor compiler generator hash: a54ca847c758bc98d8e353ad6d70088df31de1820f6cf9d1c3462505f563e6b8
// Generated with TypeScript: 6.0.3
const __nodeRequire = require;
@@ -1017,6 +1017,27 @@ const HTML_BOOLEAN_ATTRIBUTES = new Set([
function isHtmlBooleanAttribute(name) {
return HTML_BOOLEAN_ATTRIBUTES.has(name.toLowerCase());
}
/*
* Constraint attributes that must be absent rather than empty. `pattern` is the
* dangerous one: 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. A component declaring `pattern: string = ""` and rendering
* `pattern="{pattern}"` therefore breaks every input that did not ask for a
* pattern. The rest are inert when empty, but carry no meaning either.
*/
const OMIT_WHEN_EMPTY_ATTRIBUTES = new Set([
"pattern",
"minlength",
"maxlength",
"min",
"max",
"step",
"inputmode",
"accept",
]);
function isOmitWhenEmptyAttribute(name) {
return OMIT_WHEN_EMPTY_ATTRIBUTES.has(name.toLowerCase());
}
const URL_ATTRIBUTES = new Set([
"href",
"src",
@@ -1078,9 +1099,13 @@ function renderAttr(attr) {
case "csrText":
return "";
default:
return attr.boolean
? ` ${attr.name}`
: ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, attr.value))}"`;
if (attr.boolean)
return ` ${attr.name}`;
// An empty constraint attribute is worse than absent -- see
// OMIT_WHEN_EMPTY_ATTRIBUTES.
if (attr.value === "" && isOmitWhenEmptyAttribute(attr.name))
return "";
return ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, attr.value))}"`;
}
}
function eventAttribute(name) {
@@ -2964,6 +2989,18 @@ function renderComponentNode(node, ctx) {
const compiledValue = isExplicitComponentMount && wholeExpression
? `\${__wrnProp(${elementContext.resolveExpr(wholeExpression)})}`
: compileAttrValue(a.value, elementContext);
/*
* A constraint attribute whose value is only known at render time (a
* component prop defaulting to "") must be decided at render time, so
* the whole attribute -- name included -- is emitted by the helper.
* Component mounts are excluded: there the value is a prop being passed
* down, not an HTML attribute being written.
*/
if (isOmitWhenEmptyAttribute(a.name) && !isExplicitComponentMount) {
if (a.value === "")
return "";
return `\${__wrnOptionalAttr(${JSON.stringify(a.name)}, \`${compiledValue}\`)}`;
}
const rendered = ` ${a.name}="${compiledValue}"`;
const referencesState = exprRefsComponentReactiveValue(a.value, ctx);
const referencesLoopVariable = elementContext.loopVars
@@ -3264,6 +3301,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,
@@ -3415,6 +3456,9 @@ function wholeAttributeExpression(value) {
function __wrnHtml(v) {
return String(v == null ? "" : v).replace(/[&<>]/g, (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : "&gt;");
}
function __wrnOptionalAttr(name, value) {
return value === "" ? "" : " " + name + '="' + value + '"';
}
function __wrnAttr(v) {
return String(v == null ? "" : v).replace(/[&<>"]/g, (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : "&quot;");
}
+1 -1
View File
@@ -1,4 +1,4 @@
// WRN editor extension source hash: 6c652a0d490112180d2167de7a6bbc8ba179343ee313c4b51812958bcd941b83
// WRN editor extension source hash: 59ff54353ed4f50aa53f3731e232726d55549936b2edf8958beb407cc4fdf9d2
// WRN editor extension generator hash: 456d1d614e44e5fb1f19b784176c09cf2ade9b64ef73a17934c2698150b62728
"use strict";
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);