chore: fit the loop-locals fix within the production gate
Three things the gate caught that the test suite could not.
The runtime size budget: writing `data-wrn-loop-locals` on client-rendered
loop items pushed reactive-runtime.ts to 51,603 against a 51,400 budget that
had only 125 bytes of headroom. Trimmed the encoder to the
btoa/encodeURIComponent idiom, recovering 65 bytes and leaving the smallest
form that still handles non-ASCII, then raised the budget to 51,600 with the
reason recorded in the file's own convention -- the remaining 263 bytes buy a
correctness fix, not a feature.
The VS Code extension bundles its own copy of the compiler, so the syntax and
compiler fixes made it stale. Rebuilt.
And a bug in the new test: `\{` inside a template literal is an unnecessary
escape, so the "brace inside a regex" case was testing an unescaped brace.
`\{` tests the case it was written for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+165
-17
@@ -1,6 +1,6 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly.
|
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly.
|
||||||
// WRN editor compiler source hash: 3dd8f4ba41eb3b4086c63e530a5daa985c1591e76e23bdd0fc47c87eefe5a4a6
|
// WRN editor compiler source hash: 0ce7440feda082e0baf1ad3a8f2a5c309f308b755bd074fa8cab6de16efb675e
|
||||||
// WRN editor compiler generator hash: a54ca847c758bc98d8e353ad6d70088df31de1820f6cf9d1c3462505f563e6b8
|
// WRN editor compiler generator hash: a54ca847c758bc98d8e353ad6d70088df31de1820f6cf9d1c3462505f563e6b8
|
||||||
// Generated with TypeScript: 6.0.3
|
// Generated with TypeScript: 6.0.3
|
||||||
const __nodeRequire = require;
|
const __nodeRequire = require;
|
||||||
@@ -1291,15 +1291,15 @@ function bakeLoopAttr(raw, typed = false) {
|
|||||||
return out + escLit(attrEscape(raw.slice(last)));
|
return out + escLit(attrEscape(raw.slice(last)));
|
||||||
}
|
}
|
||||||
/** Render one loop-body node to template-literal source (nested loops inline). */
|
/** Render one loop-body node to template-literal source (nested loops inline). */
|
||||||
function renderLoopBody(node) {
|
function renderLoopBody(node, locals = []) {
|
||||||
if (node.type === "text") {
|
if (node.type === "text") {
|
||||||
return bakeLoopText(node.value);
|
return bakeLoopText(node.value);
|
||||||
}
|
}
|
||||||
if (node.type === "each") {
|
if (node.type === "each") {
|
||||||
return compileEachExpr(node);
|
return compileEachExpr(node, locals);
|
||||||
}
|
}
|
||||||
if (node.type === "if") {
|
if (node.type === "if") {
|
||||||
return compileIfExpr(node);
|
return compileIfExpr(node, locals);
|
||||||
}
|
}
|
||||||
const componentTag = isComponentTag(node.tag);
|
const componentTag = isComponentTag(node.tag);
|
||||||
const attrs = node.attrs
|
const attrs = node.attrs
|
||||||
@@ -1316,7 +1316,13 @@ function renderLoopBody(node) {
|
|||||||
return escLit(` ${name}="`) + bakeLoopAttr(attr.value, componentTag) + escLit(`"`);
|
return escLit(` ${name}="`) + bakeLoopAttr(attr.value, componentTag) + escLit(`"`);
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
const inner = node.children.map(renderLoopBody).join("");
|
// A handler expression is emitted as text and evaluated at event time, so any
|
||||||
|
// `{#each}` variable it names has to travel with the element. The runtime
|
||||||
|
// resolves them with closest("[data-wrn-loop-locals]"). Only elements that
|
||||||
|
// actually bind an event need it -- marking every node would bloat the HTML.
|
||||||
|
const localsAttr = locals.length > 0 && node.attrs.some((attr) => attr.event) ? loopLocalsAttr(locals) : "";
|
||||||
|
const inner = node.children.map((child) => renderLoopBody(child, locals)).join("");
|
||||||
|
const openAttrs = attrs + localsAttr;
|
||||||
if (node.tag === "Static")
|
if (node.tag === "Static")
|
||||||
return inner;
|
return inner;
|
||||||
if (node.tag === "Dynamic")
|
if (node.tag === "Dynamic")
|
||||||
@@ -1360,26 +1366,37 @@ function renderLoopBody(node) {
|
|||||||
if (island)
|
if (island)
|
||||||
return escLit(island);
|
return escLit(island);
|
||||||
return (escLit(`<div data-component="${attrEscape(node.tag)}"`) +
|
return (escLit(`<div data-component="${attrEscape(node.tag)}"`) +
|
||||||
attrs +
|
openAttrs +
|
||||||
escLit(">") +
|
escLit(">") +
|
||||||
inner +
|
inner +
|
||||||
escLit("</div>"));
|
escLit("</div>"));
|
||||||
}
|
}
|
||||||
if (syntax_1.VOID_ELEMENTS.has(node.tag.toLowerCase())) {
|
if (syntax_1.VOID_ELEMENTS.has(node.tag.toLowerCase())) {
|
||||||
return escLit(`<${node.tag}`) + attrs + escLit(">");
|
return escLit(`<${node.tag}`) + openAttrs + escLit(">");
|
||||||
}
|
}
|
||||||
return escLit(`<${node.tag}`) + attrs + escLit(">") + inner + escLit(`</${node.tag}>`);
|
return escLit(`<${node.tag}`) + openAttrs + escLit(">") + inner + escLit(`</${node.tag}>`);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Build the ` data-wrn-loop-locals="..."` attribute for a page-rendered loop
|
||||||
|
* body. Deliberately not passed through escLit: the `${...}` must stay live so
|
||||||
|
* the values are encoded at render time.
|
||||||
|
*/
|
||||||
|
function loopLocalsAttr(locals) {
|
||||||
|
const entries = locals.map((name) => `${JSON.stringify(name)}: ${name}`).join(", ");
|
||||||
|
return ` data-wrn-loop-locals="\${__wrnexusEncodeLoopLocals({ ${entries} })}"`;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Compile a `{#each list as item}` block to a `${…}` template-literal interpolation
|
* Compile a `{#each list as item}` block to a `${…}` template-literal interpolation
|
||||||
* that iterates the (server-evaluated) list and joins the per-item body. `list` is a
|
* that iterates the (server-evaluated) list and joins the per-item body. `list` is a
|
||||||
* JS expression evaluated where `ssr` data bindings are in scope as raw named values.
|
* JS expression evaluated where `ssr` data bindings are in scope as raw named values.
|
||||||
*/
|
*/
|
||||||
function compileEachExpr(node) {
|
function compileEachExpr(node, outerLocals = []) {
|
||||||
const item = node.item;
|
const item = node.item;
|
||||||
const index = node.index ?? "__wi";
|
const index = node.index ?? "__wi";
|
||||||
const body = node.body.map(renderLoopBody).join("");
|
// A nested loop can reference the outer loop's variables too.
|
||||||
const empty = node.empty.map(renderLoopBody).join("");
|
const locals = [...outerLocals, item, index];
|
||||||
|
const body = node.body.map((child) => renderLoopBody(child, locals)).join("");
|
||||||
|
const empty = node.empty.map((child) => renderLoopBody(child, outerLocals)).join("");
|
||||||
return ("${(() => { const __wl = Array.isArray(" +
|
return ("${(() => { const __wl = Array.isArray(" +
|
||||||
node.list +
|
node.list +
|
||||||
") ? (" +
|
") ? (" +
|
||||||
@@ -1399,11 +1416,11 @@ function compileEachExpr(node) {
|
|||||||
* that renders the first truthy branch's body (or the `{:else}` body, or "" when neither).
|
* that renders the first truthy branch's body (or the `{:else}` body, or "" when neither).
|
||||||
* Conditions are JS expressions evaluated in the surrounding server scope.
|
* Conditions are JS expressions evaluated in the surrounding server scope.
|
||||||
*/
|
*/
|
||||||
function compileIfExpr(node) {
|
function compileIfExpr(node, locals = []) {
|
||||||
let expr = "``"; // no matching branch → empty string
|
let expr = "``"; // no matching branch → empty string
|
||||||
for (let k = node.branches.length - 1; k >= 0; k--) {
|
for (let k = node.branches.length - 1; k >= 0; k--) {
|
||||||
const b = node.branches[k];
|
const b = node.branches[k];
|
||||||
const bodySrc = "`" + b.body.map(renderLoopBody).join("") + "`";
|
const bodySrc = "`" + b.body.map((child) => renderLoopBody(child, locals)).join("") + "`";
|
||||||
expr = b.cond === null ? bodySrc : "(" + b.cond + ") ? " + bodySrc + " : " + expr;
|
expr = b.cond === null ? bodySrc : "(" + b.cond + ") ? " + bodySrc + " : " + expr;
|
||||||
}
|
}
|
||||||
return "${" + expr + "}";
|
return "${" + expr + "}";
|
||||||
@@ -2282,6 +2299,18 @@ function generateInner(ast) {
|
|||||||
if (needsRuntimeHelpers) {
|
if (needsRuntimeHelpers) {
|
||||||
out.push(`import { buildApiRequest as __wrnexusBuildApiRequest } from "@wrnexus/core";`);
|
out.push(`import { buildApiRequest as __wrnexusBuildApiRequest } from "@wrnexus/core";`);
|
||||||
out.push(ssrRuntimeSource());
|
out.push(ssrRuntimeSource());
|
||||||
|
// Loop bodies that bind an event carry their {#each} locals in an encoded
|
||||||
|
// attribute. Emitted only when the view actually produced one, so a page
|
||||||
|
// without handlers in a loop keeps the smaller prelude -- but it MUST be
|
||||||
|
// emitted whenever the marker is, or the render throws on an undefined
|
||||||
|
// function instead of the handler throwing on an undefined variable.
|
||||||
|
if (body.includes("__wrnexusEncodeLoopLocals(")) {
|
||||||
|
out.push(`function __wrnexusEncodeLoopLocals(value: Record<string, unknown>): string {
|
||||||
|
const json = JSON.stringify(value);
|
||||||
|
const buffer = (globalThis as { Buffer?: { from(i: string, e: string): { toString(e: string): string } } }).Buffer;
|
||||||
|
return buffer ? buffer.from(json, "utf8").toString("base64") : btoa(unescape(encodeURIComponent(json)));
|
||||||
|
}`);
|
||||||
|
}
|
||||||
out.push(`const __wrnexusSsrBindings: __WrnexusSsrBinding[] = ${JSON.stringify(ssrBindings, null, 2)};`);
|
out.push(`const __wrnexusSsrBindings: __WrnexusSsrBinding[] = ${JSON.stringify(ssrBindings, null, 2)};`);
|
||||||
}
|
}
|
||||||
if (hasServerApis) {
|
if (hasServerApis) {
|
||||||
@@ -7477,6 +7506,88 @@ exports.isIdentPart = isIdentPart;
|
|||||||
* reimplementing it — a second hand-rolled scanner is how apostrophes in
|
* reimplementing it — a second hand-rolled scanner is how apostrophes in
|
||||||
* prose used to swallow braces.
|
* prose used to swallow braces.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Identifiers that can precede a `/` without ending an expression, so the `/`
|
||||||
|
* opens a regex rather than dividing.
|
||||||
|
*/
|
||||||
|
const REGEX_PRECEDING_KEYWORDS = new Set([
|
||||||
|
"return",
|
||||||
|
"typeof",
|
||||||
|
"instanceof",
|
||||||
|
"in",
|
||||||
|
"of",
|
||||||
|
"new",
|
||||||
|
"delete",
|
||||||
|
"void",
|
||||||
|
"do",
|
||||||
|
"else",
|
||||||
|
"yield",
|
||||||
|
"await",
|
||||||
|
"case",
|
||||||
|
]);
|
||||||
|
/**
|
||||||
|
* Decide whether the `/` at `i` opens a regex literal or is a division sign.
|
||||||
|
*
|
||||||
|
* Scans backwards for the last significant character. `a / b` divides; `(/a/)`,
|
||||||
|
* `= /a/` and `return /a/` do not. Erring towards division is the safe
|
||||||
|
* direction -- mistaking division for a regex would swallow everything to the
|
||||||
|
* next `/` and lose any braces in between.
|
||||||
|
*/
|
||||||
|
function opensRegex(src, i) {
|
||||||
|
let j = i - 1;
|
||||||
|
while (j >= 0 && (src[j] === " " || src[j] === "\t" || src[j] === "\r" || src[j] === "\n"))
|
||||||
|
j--;
|
||||||
|
if (j < 0)
|
||||||
|
return true;
|
||||||
|
const prev = src[j];
|
||||||
|
if (/[A-Za-z0-9_$]/.test(prev)) {
|
||||||
|
// An identifier ends an expression, so `/` divides -- unless it is a
|
||||||
|
// keyword that cannot end one, like `return`.
|
||||||
|
let k = j;
|
||||||
|
while (k >= 0 && /[A-Za-z0-9_$]/.test(src[k]))
|
||||||
|
k--;
|
||||||
|
return REGEX_PRECEDING_KEYWORDS.has(src.slice(k + 1, j + 1));
|
||||||
|
}
|
||||||
|
// `)` and `]` close an expression, `.` continues one, and a quote ends a
|
||||||
|
// literal; anything else leaves us in a position where a regex may start.
|
||||||
|
return (prev !== ")" && prev !== "]" && prev !== "." && prev !== '"' && prev !== "'" && prev !== "`");
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Scan a regex literal starting at `i`, returning the index just past its
|
||||||
|
* closing `/` and flags, or null when this is not in fact a regex.
|
||||||
|
*
|
||||||
|
* A regex literal cannot span a newline, so an unterminated one is treated as
|
||||||
|
* "not a regex" rather than swallowing the rest of the file. That is what keeps
|
||||||
|
* a bare URL in view text (`https://example.com/a//b`) intact.
|
||||||
|
*/
|
||||||
|
function skipRegex(src, i) {
|
||||||
|
let j = i + 1;
|
||||||
|
let inClass = false;
|
||||||
|
while (j < src.length) {
|
||||||
|
const c = src[j];
|
||||||
|
if (c === "\n")
|
||||||
|
return null;
|
||||||
|
if (c === "\\") {
|
||||||
|
j += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inClass) {
|
||||||
|
if (c === "]")
|
||||||
|
inClass = false;
|
||||||
|
}
|
||||||
|
else if (c === "[") {
|
||||||
|
inClass = true;
|
||||||
|
}
|
||||||
|
else if (c === "/") {
|
||||||
|
j++;
|
||||||
|
while (j < src.length && /[a-z]/.test(src[j]))
|
||||||
|
j++;
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
function skipLiteralOrComment(src, i, atLineStart) {
|
function skipLiteralOrComment(src, i, atLineStart) {
|
||||||
const c = src[i];
|
const c = src[i];
|
||||||
if (c === "/" && src[i + 1] === "*") {
|
if (c === "/" && src[i + 1] === "*") {
|
||||||
@@ -7500,6 +7611,13 @@ function skipLiteralOrComment(src, i, atLineStart) {
|
|||||||
}
|
}
|
||||||
return src.length;
|
return src.length;
|
||||||
}
|
}
|
||||||
|
// A regex literal is neither a string nor a brace pair, but it can contain
|
||||||
|
// both. Without this, a quote inside one opened a phantom string that
|
||||||
|
// swallowed every brace to the next quote, and a lone `{`/`}` miscounted
|
||||||
|
// block depth.
|
||||||
|
if (c === "/" && opensRegex(src, i)) {
|
||||||
|
return skipRegex(src, i);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
class Lexer {
|
class Lexer {
|
||||||
@@ -7508,8 +7626,13 @@ class Lexer {
|
|||||||
constructor(src) {
|
constructor(src) {
|
||||||
this.src = src;
|
this.src = src;
|
||||||
}
|
}
|
||||||
/** Skip whitespace and `// line comments`. */
|
/**
|
||||||
skipTrivia() {
|
* Skip whitespace and `// line comments`, but NOT block comments.
|
||||||
|
*
|
||||||
|
* Kept separate from `skipTrivia` so `startsWithBlockComment` can still see a
|
||||||
|
* block comment that `skipTrivia` would otherwise consume.
|
||||||
|
*/
|
||||||
|
skipWhitespaceAndLineComments() {
|
||||||
const { src } = this;
|
const { src } = this;
|
||||||
while (this.pos < src.length) {
|
while (this.pos < src.length) {
|
||||||
const c = src[this.pos];
|
const c = src[this.pos];
|
||||||
@@ -7525,9 +7648,34 @@ class Lexer {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/** True when the next non-trivia characters open a block comment. */
|
/**
|
||||||
|
* Skip whitespace, line comments and block comments.
|
||||||
|
*
|
||||||
|
* Block comments used to be skipped only inside a braced body, so one written
|
||||||
|
* between two members failed with a bare "Unexpected character '/'" -- the
|
||||||
|
* same comment parsed or did not depending on where it sat.
|
||||||
|
*/
|
||||||
|
skipTrivia() {
|
||||||
|
const { src } = this;
|
||||||
|
while (this.pos < src.length) {
|
||||||
|
this.skipWhitespaceAndLineComments();
|
||||||
|
if (src[this.pos] === "/" && src[this.pos + 1] === "*") {
|
||||||
|
const close = src.indexOf("*/", this.pos + 2);
|
||||||
|
this.pos = close === -1 ? src.length : close + 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* True when the next non-trivia characters open a block comment.
|
||||||
|
*
|
||||||
|
* Deliberately skips only whitespace and line comments: `props {}` refuses
|
||||||
|
* block comments with an explained error, and that check must run before
|
||||||
|
* `skipTrivia` would swallow the comment and drop a declaration silently.
|
||||||
|
*/
|
||||||
startsWithBlockComment() {
|
startsWithBlockComment() {
|
||||||
this.skipTrivia();
|
this.skipWhitespaceAndLineComments();
|
||||||
return this.src[this.pos] === "/" && this.src[this.pos + 1] === "*";
|
return this.src[this.pos] === "/" && this.src[this.pos + 1] === "*";
|
||||||
}
|
}
|
||||||
/** Read and consume the next structural token. */
|
/** Read and consume the next structural token. */
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// WRN editor extension source hash: 59ff54353ed4f50aa53f3731e232726d55549936b2edf8958beb407cc4fdf9d2
|
// WRN editor extension source hash: e2d214e98eb953756974d4b731530ed432c2f9dca6f0aaee975453b2eec84d75
|
||||||
// WRN editor extension generator hash: 456d1d614e44e5fb1f19b784176c09cf2ade9b64ef73a17934c2698150b62728
|
// WRN editor extension generator hash: 456d1d614e44e5fb1f19b784176c09cf2ade9b64ef73a17934c2698150b62728
|
||||||
"use strict";
|
"use strict";
|
||||||
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// WRN editor language server source hash: e55270eb33d7722a82d13d3a79097fea77eca69612aeba104e9cbdd8fc67e8c6
|
// WRN editor language server source hash: cea070c2eebb5339c7b48f4200d34e51f2adb16eba4d6d3f92e5040081c5b079
|
||||||
// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
|
// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
|
||||||
// @bun @bun-cjs
|
// @bun @bun-cjs
|
||||||
(function(exports, require, module, __filename, __dirname) {var __create = Object.create;
|
(function(exports, require, module, __filename, __dirname) {var __create = Object.create;
|
||||||
@@ -169671,6 +169671,64 @@ var isWs = (c) => c === " " || c === "\t" || c === `
|
|||||||
` || c === "\r";
|
` || c === "\r";
|
||||||
var isIdentStart = (c) => /[A-Za-z_]/.test(c);
|
var isIdentStart = (c) => /[A-Za-z_]/.test(c);
|
||||||
var isIdentPart = (c) => /[A-Za-z0-9_]/.test(c);
|
var isIdentPart = (c) => /[A-Za-z0-9_]/.test(c);
|
||||||
|
var REGEX_PRECEDING_KEYWORDS = new Set([
|
||||||
|
"return",
|
||||||
|
"typeof",
|
||||||
|
"instanceof",
|
||||||
|
"in",
|
||||||
|
"of",
|
||||||
|
"new",
|
||||||
|
"delete",
|
||||||
|
"void",
|
||||||
|
"do",
|
||||||
|
"else",
|
||||||
|
"yield",
|
||||||
|
"await",
|
||||||
|
"case"
|
||||||
|
]);
|
||||||
|
function opensRegex(src, i) {
|
||||||
|
let j = i - 1;
|
||||||
|
while (j >= 0 && (src[j] === " " || src[j] === "\t" || src[j] === "\r" || src[j] === `
|
||||||
|
`))
|
||||||
|
j--;
|
||||||
|
if (j < 0)
|
||||||
|
return true;
|
||||||
|
const prev = src[j];
|
||||||
|
if (/[A-Za-z0-9_$]/.test(prev)) {
|
||||||
|
let k = j;
|
||||||
|
while (k >= 0 && /[A-Za-z0-9_$]/.test(src[k]))
|
||||||
|
k--;
|
||||||
|
return REGEX_PRECEDING_KEYWORDS.has(src.slice(k + 1, j + 1));
|
||||||
|
}
|
||||||
|
return prev !== ")" && prev !== "]" && prev !== "." && prev !== '"' && prev !== "'" && prev !== "`";
|
||||||
|
}
|
||||||
|
function skipRegex(src, i) {
|
||||||
|
let j = i + 1;
|
||||||
|
let inClass = false;
|
||||||
|
while (j < src.length) {
|
||||||
|
const c = src[j];
|
||||||
|
if (c === `
|
||||||
|
`)
|
||||||
|
return null;
|
||||||
|
if (c === "\\") {
|
||||||
|
j += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inClass) {
|
||||||
|
if (c === "]")
|
||||||
|
inClass = false;
|
||||||
|
} else if (c === "[") {
|
||||||
|
inClass = true;
|
||||||
|
} else if (c === "/") {
|
||||||
|
j++;
|
||||||
|
while (j < src.length && /[a-z]/.test(src[j]))
|
||||||
|
j++;
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
function skipLiteralOrComment(src, i, atLineStart) {
|
function skipLiteralOrComment(src, i, atLineStart) {
|
||||||
const c = src[i];
|
const c = src[i];
|
||||||
if (c === "/" && src[i + 1] === "*") {
|
if (c === "/" && src[i + 1] === "*") {
|
||||||
@@ -169695,6 +169753,9 @@ function skipLiteralOrComment(src, i, atLineStart) {
|
|||||||
}
|
}
|
||||||
return src.length;
|
return src.length;
|
||||||
}
|
}
|
||||||
|
if (c === "/" && opensRegex(src, i)) {
|
||||||
|
return skipRegex(src, i);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169704,7 +169765,7 @@ class Lexer {
|
|||||||
constructor(src) {
|
constructor(src) {
|
||||||
this.src = src;
|
this.src = src;
|
||||||
}
|
}
|
||||||
skipTrivia() {
|
skipWhitespaceAndLineComments() {
|
||||||
const { src } = this;
|
const { src } = this;
|
||||||
while (this.pos < src.length) {
|
while (this.pos < src.length) {
|
||||||
const c = src[this.pos];
|
const c = src[this.pos];
|
||||||
@@ -169721,8 +169782,20 @@ class Lexer {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
skipTrivia() {
|
||||||
|
const { src } = this;
|
||||||
|
while (this.pos < src.length) {
|
||||||
|
this.skipWhitespaceAndLineComments();
|
||||||
|
if (src[this.pos] === "/" && src[this.pos + 1] === "*") {
|
||||||
|
const close = src.indexOf("*/", this.pos + 2);
|
||||||
|
this.pos = close === -1 ? src.length : close + 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
startsWithBlockComment() {
|
startsWithBlockComment() {
|
||||||
this.skipTrivia();
|
this.skipWhitespaceAndLineComments();
|
||||||
return this.src[this.pos] === "/" && this.src[this.pos + 1] === "*";
|
return this.src[this.pos] === "/" && this.src[this.pos + 1] === "*";
|
||||||
}
|
}
|
||||||
next() {
|
next() {
|
||||||
|
|||||||
@@ -999,20 +999,11 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
var json = JSON.stringify(locals);
|
var json = JSON.stringify(locals);
|
||||||
if (json === undefined) return;
|
if (json === undefined) return;
|
||||||
|
|
||||||
var bytes = new TextEncoder().encode(json);
|
node.setAttribute(
|
||||||
var binary = "";
|
"data-wrn-loop-locals",
|
||||||
|
window.btoa(unescape(encodeURIComponent(json))),
|
||||||
for (var index = 0; index < bytes.length; index++) {
|
);
|
||||||
binary += String.fromCharCode(bytes[index]);
|
|
||||||
}
|
|
||||||
|
|
||||||
node.setAttribute("data-wrn-loop-locals", window.btoa(binary));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
/*
|
|
||||||
* A value that will not serialise (a cycle, a DOM node) must not take
|
|
||||||
* the whole loop down -- the item still renders, and a handler naming
|
|
||||||
* that local fails on its own terms rather than silently.
|
|
||||||
*/
|
|
||||||
console.error("[wrnexus] failed to encode loop locals", error);
|
console.error("[wrnexus] failed to encode loop locals", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ ${body}
|
|||||||
expect(parse(mk(` const x = /it's/.test("its");`)).name).toBe("P");
|
expect(parse(mk(` const x = /it's/.test("its");`)).name).toBe("P");
|
||||||
expect(parse(mk(` const x = /"/.test("q");`)).name).toBe("P");
|
expect(parse(mk(` const x = /"/.test("q");`)).name).toBe("P");
|
||||||
// A brace inside a regex used to be counted as block depth.
|
// A brace inside a regex used to be counted as block depth.
|
||||||
expect(parse(mk(` const x = /\{/.test("{");`)).name).toBe("P");
|
expect(parse(mk(` const x = /\\{/.test("{");`)).name).toBe("P");
|
||||||
expect(parse(mk(` const x = /}/.test("}");`)).name).toBe("P");
|
expect(parse(mk(` const x = /}/.test("}");`)).name).toBe("P");
|
||||||
// A brace quantifier is balanced, but must not be counted either.
|
// A brace quantifier is balanced, but must not be counted either.
|
||||||
expect(parse(mk(` const x = /^a{2,3}$/.test("aa");`)).name).toBe("P");
|
expect(parse(mk(` const x = /^a{2,3}$/.test("aa");`)).name).toBe("P");
|
||||||
|
|||||||
@@ -183,7 +183,15 @@ const runtimeBudgets = {
|
|||||||
*/
|
*/
|
||||||
// Raised to 51_400: the callApi transport (query building, CSRF header,
|
// Raised to 51_400: the callApi transport (query building, CSRF header,
|
||||||
// JSON body, success/failure contract) for compiled api blocks bought ~1,025 bytes.
|
// JSON body, success/failure contract) for compiled api blocks bought ~1,025 bytes.
|
||||||
"reactive-runtime.ts": 51_400,
|
//
|
||||||
|
// Raised to 51_600 on 2026-08-22: writing `data-wrn-loop-locals` onto
|
||||||
|
// client-rendered for-loop items bought 263 bytes. Without it a component's
|
||||||
|
// output binding inside a loop resolved no locals and silently dropped every
|
||||||
|
// call, while a plain DOM handler in the same position worked -- so the cost
|
||||||
|
// buys a correctness fix, not a feature. The encoder was trimmed to the
|
||||||
|
// btoa/encodeURIComponent idiom first, which recovered 65 of those bytes;
|
||||||
|
// what remains is the smallest form that still handles non-ASCII.
|
||||||
|
"reactive-runtime.ts": 51_600,
|
||||||
"component-controllers.ts": 24_100,
|
"component-controllers.ts": 24_100,
|
||||||
"nav-runtime.ts": 12_000,
|
"nav-runtime.ts": 12_000,
|
||||||
"realtime-runtime.ts": 8_000,
|
"realtime-runtime.ts": 8_000,
|
||||||
|
|||||||
Reference in New Issue
Block a user