fix: isolate test globals, close tags at every caret, trim the runtime

Three pre-existing issues that the previous commit worked around rather
than solved.

Test global pollution. packages/csr's suites install a happy-dom window
over the real globals and delete them before each test. bun test runs one
file at a time, so those deletions outlived the file and later suites
failed with "fetch is not a function" -- 20 failures from `bun test` with
no argument. They now restore what they captured. The editor's Node tests
shim the vscode host by patching Module._load, which Bun's resolver does
not consult; the shim registers a virtual module under Bun instead, so the
same files pass under both runners.

Multi-cursor tag auto-close. The handler now closes the tag at every
caret. Positions come from the editor's selections rather than the change
ranges, which are in pre-edit coordinates and are short by the preceding
insertions once several carets share a line. One insertSnippet call
carries them all, since inserting sequentially would collapse the
selection to the first snippet. Carets wanting different closing tags are
declined rather than half-applied. Moved to its own module so it can be
tested without loading the language client.

Runtime size. Trimmed 2,414 bytes: the global lookup tables became one
prototype-safe scheme (a name like "toString" was previously a hit on
Object.prototype), shared hasOwn/toArray/pairBinding helpers replaced the
repeated chains, and dead code went. That was everything available without
dropping or deferring a feature -- 49,000 was not reachable, so the budget
is now 50,500, set just above the real figure so future growth trips it.

Two tests changed: one asserted on runtime source text and now asserts the
timers resolve; a new one covers reactive class bindings inside data-for,
which the enclosing loop effect tracks rather than each binding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 10:14:18 +05:30
co-authored by Claude Opus 5
parent ac248f2bb0
commit a20f143acb
16 changed files with 650 additions and 393 deletions
+83
View File
@@ -0,0 +1,83 @@
"use strict";
const vscode = require("vscode");
/**
* Auto-close tags as they are typed.
*
* LSP has no request for this, so the client watches document changes and asks
* the server whether the tag should close. The server owns the decision because
* void elements and already-closed tags must not be closed.
*/
function registerAutoCloseTags(context, client) {
const listener = vscode.workspace.onDidChangeTextDocument(async (event) => {
if (event.document.languageId !== "wrn") return;
if (!vscode.workspace.getConfiguration("wrnexus.html").get("autoClosingTags", true)) return;
const changes = event.contentChanges;
if (!changes.length) return;
const typed = changes[0].text;
if (typed !== ">" && typed !== "/") return;
// Every cursor must have typed the same trigger. A replaced selection
// (overtype, or select-and-type) is declined rather than guessed at.
if (!changes.every((change) => change.text === typed && change.rangeLength === 0)) return;
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document !== event.document) return;
/*
* Positions come from the editor's selections, not from the changes.
*
* A change's `range` is in coordinates from before the whole event, so with
* several cursors on one line every range after the first is short by the
* insertions preceding it. The selections have already been adjusted for
* the edit, so they are where the carets actually are.
*/
const positions = editor.selections.map((selection) => selection.active);
if (positions.length !== changes.length) return;
if (!editor.selections.every((selection) => selection.isEmpty)) return;
const documentVersion = event.document.version;
const snippets = await Promise.all(
positions.map((position) =>
client.sendRequest("wrn/tagComplete", {
textDocument: { uri: event.document.uri.toString() },
position: { line: position.line, character: position.character },
}),
),
);
if (!snippets.every((snippet) => typeof snippet === "string" && snippet)) return;
/*
* One insertSnippet call carries one snippet, and it is the only form that
* keeps every caret: inserting sequentially would collapse the selection to
* the first snippet and invalidate the remaining positions. Cursors that
* want different closing tags are therefore declined rather than
* half-applied -- multi-cursor editing of matching lines, which is what
* this is for, produces one snippet for all of them.
*/
if (!snippets.every((snippet) => snippet === snippets[0])) return;
// The user may have kept typing during the round-trip; re-validate everything the
// insertion depends on before touching the document, since a stale offset would
// silently corrupt it.
if (vscode.window.activeTextEditor !== editor) return;
if (editor.document !== event.document) return;
if (editor.document.version !== documentVersion) return;
if (editor.selections.length !== positions.length) return;
if (
!editor.selections.every(
(selection, index) => selection.isEmpty && selection.active.isEqual(positions[index]),
)
) {
return;
}
await editor.insertSnippet(new vscode.SnippetString(snippets[0]), positions);
});
context.subscriptions.push(listener);
}
module.exports = { registerAutoCloseTags };
+54 -39
View File
@@ -1,4 +1,4 @@
// WRN editor extension source hash: f74c11de70974caa6fb0cb4ee90ec39c10a924951733dadcc346b308be0e13d3 // WRN editor extension source hash: 63bce75e2686c7586a3a08b8811ebc681d2265fdfe54e984630614e3dcef21f5
// 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);
@@ -22701,10 +22701,63 @@ var require_main5 = __commonJS((exports2) => {
} }
}); });
// editors/vscode/src/auto-close-tags.js
var require_auto_close_tags = __commonJS((exports2, module2) => {
var vscode = require("vscode");
function registerAutoCloseTags(context, client) {
const listener = vscode.workspace.onDidChangeTextDocument(async (event) => {
if (event.document.languageId !== "wrn")
return;
if (!vscode.workspace.getConfiguration("wrnexus.html").get("autoClosingTags", true))
return;
const changes = event.contentChanges;
if (!changes.length)
return;
const typed = changes[0].text;
if (typed !== ">" && typed !== "/")
return;
if (!changes.every((change) => change.text === typed && change.rangeLength === 0))
return;
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document !== event.document)
return;
const positions = editor.selections.map((selection) => selection.active);
if (positions.length !== changes.length)
return;
if (!editor.selections.every((selection) => selection.isEmpty))
return;
const documentVersion = event.document.version;
const snippets = await Promise.all(positions.map((position) => client.sendRequest("wrn/tagComplete", {
textDocument: { uri: event.document.uri.toString() },
position: { line: position.line, character: position.character }
})));
if (!snippets.every((snippet) => typeof snippet === "string" && snippet))
return;
if (!snippets.every((snippet) => snippet === snippets[0]))
return;
if (vscode.window.activeTextEditor !== editor)
return;
if (editor.document !== event.document)
return;
if (editor.document.version !== documentVersion)
return;
if (editor.selections.length !== positions.length)
return;
if (!editor.selections.every((selection, index) => selection.isEmpty && selection.active.isEqual(positions[index]))) {
return;
}
await editor.insertSnippet(new vscode.SnippetString(snippets[0]), positions);
});
context.subscriptions.push(listener);
}
module2.exports = { registerAutoCloseTags };
});
// editors/vscode/src/extension.js // editors/vscode/src/extension.js
var path = require("node:path"); var path = require("node:path");
var vscode = require("vscode"); var vscode = require("vscode");
var { LanguageClient, TransportKind } = require_main5(); var { LanguageClient, TransportKind } = require_main5();
var { registerAutoCloseTags } = require_auto_close_tags();
var WRN_LANGUAGE_ID = "wrn"; var WRN_LANGUAGE_ID = "wrn";
var client; var client;
async function recoverWrnLanguage(document) { async function recoverWrnLanguage(document) {
@@ -22718,44 +22771,6 @@ async function recoverWrnLanguage(document) {
console.warn("[wrnexus] unable to recover .wrn language association:", error instanceof Error ? error.message : String(error)); console.warn("[wrnexus] unable to recover .wrn language association:", error instanceof Error ? error.message : String(error));
} }
} }
function registerAutoCloseTags(context, client2) {
const listener = vscode.workspace.onDidChangeTextDocument(async (event) => {
if (event.document.languageId !== "wrn")
return;
if (!vscode.workspace.getConfiguration("wrnexus.html").get("autoClosingTags", true))
return;
if (event.contentChanges.length !== 1)
return;
const change = event.contentChanges[0];
if (!change || change.text !== ">" && change.text !== "/")
return;
if (change.rangeLength !== 0)
return;
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document !== event.document)
return;
const documentVersion = event.document.version;
const position = change.range.start.translate(0, change.text.length);
if (!editor.selection.isEmpty || !editor.selection.active.isEqual(position))
return;
const snippet = await client2.sendRequest("wrn/tagComplete", {
textDocument: { uri: event.document.uri.toString() },
position: { line: position.line, character: position.character }
});
if (typeof snippet !== "string" || !snippet)
return;
if (vscode.window.activeTextEditor !== editor)
return;
if (editor.document !== event.document)
return;
if (editor.document.version !== documentVersion)
return;
if (!editor.selection.isEmpty || !editor.selection.active.isEqual(position))
return;
await editor.insertSnippet(new vscode.SnippetString(snippet), position);
});
context.subscriptions.push(listener);
}
async function activate(context) { async function activate(context) {
for (const document of vscode.workspace.textDocuments) for (const document of vscode.workspace.textDocuments)
recoverWrnLanguage(document); recoverWrnLanguage(document);
+1 -50
View File
@@ -4,6 +4,7 @@
const path = require("node:path"); const path = require("node:path");
const vscode = require("vscode"); const vscode = require("vscode");
const { LanguageClient, TransportKind } = require("vscode-languageclient/node"); const { LanguageClient, TransportKind } = require("vscode-languageclient/node");
const { registerAutoCloseTags } = require("./auto-close-tags.js");
const WRN_LANGUAGE_ID = "wrn"; const WRN_LANGUAGE_ID = "wrn";
/** @type {LanguageClient | undefined} */ /** @type {LanguageClient | undefined} */
@@ -23,56 +24,6 @@ async function recoverWrnLanguage(document) {
} }
} }
/**
* Auto-close tags as they are typed.
*
* LSP has no request for this, so the client watches document changes and asks
* the server whether the tag should close. The server owns the decision because
* void elements and already-closed tags must not be closed.
*/
function registerAutoCloseTags(context, client) {
const listener = vscode.workspace.onDidChangeTextDocument(async (event) => {
if (event.document.languageId !== "wrn") return;
if (!vscode.workspace.getConfiguration("wrnexus.html").get("autoClosingTags", true)) return;
// Multi-cursor typing reports one change per cursor. Closing only the first
// leaves the rest half-typed, and each insertion shifts the offsets the
// remaining changes were measured against, so decline the whole event.
if (event.contentChanges.length !== 1) return;
const change = event.contentChanges[0];
if (!change || (change.text !== ">" && change.text !== "/")) return;
// A replaced selection (overtype/select-and-type) makes `range.start + text.length`
// an incorrect offset for both the query and the insertion; decline rather than guess.
if (change.rangeLength !== 0) return;
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document !== event.document) return;
const documentVersion = event.document.version;
const position = change.range.start.translate(0, change.text.length);
if (!editor.selection.isEmpty || !editor.selection.active.isEqual(position)) return;
const snippet = await client.sendRequest("wrn/tagComplete", {
textDocument: { uri: event.document.uri.toString() },
position: { line: position.line, character: position.character },
});
if (typeof snippet !== "string" || !snippet) return;
// The user may have kept typing during the round-trip; re-validate everything the
// insertion depends on before touching the document, since a stale offset would
// silently corrupt it.
if (vscode.window.activeTextEditor !== editor) return;
if (editor.document !== event.document) return;
if (editor.document.version !== documentVersion) return;
if (!editor.selection.isEmpty || !editor.selection.active.isEqual(position)) return;
await editor.insertSnippet(new vscode.SnippetString(snippet), position);
});
context.subscriptions.push(listener);
}
/** @param {vscode.ExtensionContext} context */ /** @param {vscode.ExtensionContext} context */
async function activate(context) { async function activate(context) {
for (const document of vscode.workspace.textDocuments) void recoverWrnLanguage(document); for (const document of vscode.workspace.textDocuments) void recoverWrnLanguage(document);
+178
View File
@@ -0,0 +1,178 @@
"use strict";
const assert = require("node:assert");
const { test } = require("node:test");
const { installVsCodeHost } = require("./vscode-host.js");
class Position {
constructor(line, character) {
this.line = line;
this.character = character;
}
translate(lineDelta, characterDelta) {
return new Position(this.line + lineDelta, this.character + characterDelta);
}
isEqual(other) {
return this.line === other.line && this.character === other.character;
}
}
class Selection {
constructor(active) {
this.active = active;
this.anchor = active;
this.isEmpty = true;
}
}
class SnippetString {
constructor(value) {
this.value = value;
}
}
let changeListener = null;
const host = {
Position,
Selection,
SnippetString,
workspace: {
onDidChangeTextDocument(listener) {
changeListener = listener;
return { dispose() {} };
},
getConfiguration() {
return { get: (_key, fallback) => fallback };
},
},
window: { activeTextEditor: null },
};
const restoreHost = installVsCodeHost(host);
const { registerAutoCloseTags } = require("../src/auto-close-tags.js");
restoreHost();
/**
* Drive the handler the way VS Code does: the document has already been
* updated and the carets moved by the time the change event fires.
*/
function scenario({ carets, snippetFor, typed = ">" }) {
const inserted = [];
const asked = [];
const document = { languageId: "wrn", version: 1, uri: { toString: () => "file:///a.wrn" } };
const editor = {
document,
selections: carets.map((caret) => new Selection(caret)),
insertSnippet(snippet, positions) {
inserted.push({ value: snippet.value, positions });
return Promise.resolve(true);
},
};
editor.selection = editor.selections[0];
host.window.activeTextEditor = editor;
const client = {
sendRequest(_method, params) {
asked.push(params.position);
return Promise.resolve(snippetFor(params.position));
},
};
registerAutoCloseTags({ subscriptions: [] }, client);
return {
inserted,
asked,
fire: () =>
changeListener({
document,
// Pre-edit coordinates, deliberately not usable as caret positions.
contentChanges: carets.map(() => ({
text: typed,
rangeLength: 0,
range: { start: new Position(0, 0) },
})),
}),
};
}
test("closes the tag at a single caret", async () => {
const run = scenario({ carets: [new Position(1, 8)], snippetFor: () => "$0</div>" });
await run.fire();
assert.equal(run.inserted.length, 1);
assert.equal(run.inserted[0].value, "$0</div>");
assert.deepEqual(
run.inserted[0].positions.map((p) => [p.line, p.character]),
[[1, 8]],
);
});
test("closes the tag at every caret in one insertion", async () => {
// One insertSnippet call is what keeps all the carets alive: inserting
// sequentially would collapse the selection to the first snippet.
const run = scenario({
carets: [new Position(1, 8), new Position(2, 8), new Position(3, 8)],
snippetFor: () => "$0</div>",
});
await run.fire();
assert.equal(run.asked.length, 3);
assert.equal(run.inserted.length, 1);
assert.deepEqual(
run.inserted[0].positions.map((p) => [p.line, p.character]),
[
[1, 8],
[2, 8],
[3, 8],
],
);
});
test("asks about each caret's own position rather than the change ranges", async () => {
// Every contentChange above reports (0, 0). Using those would query and
// insert at the wrong offsets once more than one caret is on a line.
const run = scenario({
carets: [new Position(4, 12), new Position(9, 3)],
snippetFor: () => "$0</p>",
});
await run.fire();
assert.deepEqual(
run.asked.map((p) => [p.line, p.character]),
[
[4, 12],
[9, 3],
],
);
});
test("declines when the carets want different closing tags", async () => {
const run = scenario({
carets: [new Position(1, 8), new Position(2, 8)],
snippetFor: (position) => (position.line === 1 ? "$0</div>" : "$0</span>"),
});
await run.fire();
assert.equal(run.inserted.length, 0);
});
test("declines when any caret has no tag to close", async () => {
const run = scenario({
carets: [new Position(1, 8), new Position(2, 8)],
snippetFor: (position) => (position.line === 1 ? "$0</br>" : null),
});
await run.fire();
assert.equal(run.inserted.length, 0);
});
test("declines a replaced selection", async () => {
const run = scenario({ carets: [new Position(1, 8)], snippetFor: () => "$0</div>" });
await changeListener({
document: { languageId: "wrn", version: 1, uri: { toString: () => "file:///a.wrn" } },
contentChanges: [{ text: ">", rangeLength: 3, range: { start: new Position(1, 5) } }],
});
assert.equal(run.inserted.length, 0);
});
+39 -46
View File
@@ -2,54 +2,47 @@
const test = require("node:test"); const test = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
const Module = require("node:module"); const { installVsCodeHost } = require("./vscode-host.js");
// Mock the vscode module for unit tests const restoreHost = installVsCodeHost({
const originalLoad = Module._load; Position: class Position {
Module._load = function load(request, parent, isMain) { constructor(line, character) {
if (request === "vscode") { this.line = line;
return { this.character = character;
Position: class Position { }
constructor(line, character) { },
this.line = line; Range: class Range {
this.character = character; constructor(start, end) {
} this.start = start;
}, this.end = end;
Range: class Range { }
constructor(start, end) { },
this.start = start; CompletionItem: class CompletionItem {
this.end = end; constructor(label, kind) {
} this.label = label;
}, this.kind = kind;
CompletionItem: class CompletionItem { }
constructor(label, kind) { },
this.label = label; CompletionItemKind: {
this.kind = kind; Event: 23,
} Property: 10,
}, Function: 12,
CompletionItemKind: { Keyword: 14,
Event: 23, Variable: 13,
Property: 10, },
Function: 12, SnippetString: class SnippetString {
Keyword: 14, constructor(text) {
Variable: 13, this.value = text;
}, }
SnippetString: class SnippetString { },
constructor(text) { MarkdownString: class MarkdownString {
this.value = text; constructor(text) {
} this.value = text;
}, }
MarkdownString: class MarkdownString { },
constructor(text) { });
this.value = text;
}
},
};
}
return originalLoad.call(this, request, parent, isMain);
};
const { isInsideViewBlock, provideCompletionItems } = require("../src/completion.js"); const { isInsideViewBlock, provideCompletionItems } = require("../src/completion.js");
Module._load = originalLoad; restoreHost();
const PAGE = `page Home { const PAGE = `page Home {
view { view {
+3 -7
View File
@@ -2,17 +2,13 @@
const assert = require("node:assert"); const assert = require("node:assert");
const { test } = require("node:test"); const { test } = require("node:test");
const Module = require("node:module"); const { installVsCodeHost } = require("./vscode-host.js");
// These extraction helpers are pure, but their module also registers VS Code // These extraction helpers are pure, but their module also registers VS Code
// providers at runtime. Supply a minimal host shim for unit tests. // providers at runtime. Supply a minimal host shim for unit tests.
const originalLoad = Module._load; const restoreHost = installVsCodeHost({});
Module._load = function load(request, parent, isMain) {
if (request === "vscode") return {};
return originalLoad.call(this, request, parent, isMain);
};
const { extractRouteParams, extractStates } = require("../src/completion"); const { extractRouteParams, extractStates } = require("../src/completion");
Module._load = originalLoad; restoreHost();
test("extracts dynamic route params from filename", () => { test("extracts dynamic route params from filename", () => {
const document = { const document = {
+18 -24
View File
@@ -2,30 +2,24 @@
const assert = require("node:assert"); const assert = require("node:assert");
const { test } = require("node:test"); const { test } = require("node:test");
const Module = require("node:module"); const { installVsCodeHost } = require("./vscode-host.js");
const originalLoad = Module._load; const restoreHost = installVsCodeHost({
Module._load = function load(request, parent, isMain) { Diagnostic: class Diagnostic {
if (request === "vscode") { constructor(range, message, severity) {
return { this.range = range;
Diagnostic: class Diagnostic { this.message = message;
constructor(range, message, severity) { this.severity = severity;
this.range = range; }
this.message = message; },
this.severity = severity; DiagnosticSeverity: { Error: 0, Warning: 1 },
} Range: class Range {
}, constructor(start, end) {
DiagnosticSeverity: { Error: 0, Warning: 1 }, this.start = start;
Range: class Range { this.end = end;
constructor(start, end) { }
this.start = start; },
this.end = end; });
}
},
};
}
return originalLoad.call(this, request, parent, isMain);
};
const { const {
findTopLevelDeclaration, findTopLevelDeclaration,
maskLeadingTrivia, maskLeadingTrivia,
@@ -34,7 +28,7 @@ const {
validateLayoutUsage, validateLayoutUsage,
validateRootMembers, validateRootMembers,
} = require("../src/diagnostics"); } = require("../src/diagnostics");
Module._load = originalLoad; restoreHost();
function mockDocument() { function mockDocument() {
return { return {
+39
View File
@@ -0,0 +1,39 @@
"use strict";
/**
* Supply a stub `vscode` host so extension sources can be unit tested.
*
* These files run under `node --test` (see the package's test script), where
* patching `Module._load` is enough. A bare `bun test` from the repository
* root also picks them up by filename, and Bun resolves `require` through its
* own resolver without consulting `Module._load` -- so under Bun the same
* files failed with "Cannot find package 'vscode'". Registering a virtual
* module covers that case, leaving one shim that works under both runners.
*
* Returns a function restoring the original loader.
*/
function installVsCodeHost(stub) {
const Module = require("node:module");
if (typeof Bun !== "undefined") {
require("bun").plugin({
name: "vscode-host-stub",
setup(build) {
build.module("vscode", () => ({ exports: stub, loader: "object" }));
},
});
return () => {};
}
const originalLoad = Module._load;
Module._load = function load(request, parent, isMain) {
if (request === "vscode") return stub;
return originalLoad.call(this, request, parent, isMain);
};
return () => {
Module._load = originalLoad;
};
}
module.exports = { installVsCodeHost };
+8
View File
@@ -127,6 +127,14 @@ export function getComponentControllerRuntime(development = false): string {
var emitPinInputEvent = bridge.emitPinInputEvent; var emitPinInputEvent = bridge.emitPinInputEvent;
var parseScopeDecl = bridge.parseScopeDecl; var parseScopeDecl = bridge.parseScopeDecl;
var warnOnce = bridge.warn || function () {}; var warnOnce = bridge.warn || function () {};
// The extracted sections use these the same way the core runtime does, and
// this bundle is a separate IIFE, so it needs its own copies.
function hasOwn(target, key) {
return Object.prototype.hasOwnProperty.call(target, key);
}
function toArray(value) {
return Array.prototype.slice.call(value);
}
${sections} ${sections}
function hydrate(root) { function hydrate(root) {
var host = root || document; var host = root || document;
+124 -200
View File
@@ -26,58 +26,78 @@ export const REACTIVE_RUNTIME = String.raw`
var behaviorObserver; var behaviorObserver;
var clientModuleCache = new Map(); var clientModuleCache = new Map();
/*
* Two builtin chains the runtime reaches for constantly. Aliasing them is
* not only shorter: hasOwn keeps prototype keys from reading as data, and
* toArray is needed because a NodeList is not an Array.
*/
function hasOwn(target, key) {
return Object.prototype.hasOwnProperty.call(target, key);
}
function toArray(value) {
return Array.prototype.slice.call(value);
}
/*
* data-wrn-class-* and data-wrn-bind-* both carry a JSON ["name","expression"]
* pair. Malformed markup yields null so every caller bails the same way
* rather than each repeating the parse and the shape check.
*/
function pairBinding(value) {
var parsed;
try {
parsed = JSON.parse(value);
} catch (error) {
return null;
}
return parsed && parsed.length === 2 ? parsed : null;
}
/* /*
* Globals the expression engine resolves for client code. Kept as explicit * Globals the expression engine resolves for client code. Kept as explicit
* tables rather than falling through to window[name]: an implicit fallback * lists rather than falling through to window[name]: an implicit fallback
* would let any expression reach every global on the page (and would make a * would let any expression reach every global on the page (and would make a
* typo silently resolve to some unrelated window property) -- these lists * typo silently resolve to some unrelated window property) -- these lists
* say exactly what client code may reach. * say exactly what client code may reach.
* *
* dialogGlobals must be bound to window or the browser throws * Prototype-less so a name like "toString" or "constructor" is a miss
* "Illegal invocation" when they are called detached. * rather than a hit on Object.prototype.
*/ */
var dialogGlobals = { function nameSet(names) {
alert: 1, var set = Object.create(null);
confirm: 1,
prompt: 1,
fetch: 1,
print: 1,
open: 1,
scrollTo: 1,
scrollBy: 1,
matchMedia: 1,
getComputedStyle: 1,
structuredClone: 1,
queueMicrotask: 1,
btoa: 1,
atob: 1,
};
// Language builtins. Wrapped in thunks so referencing one that a given names.split(" ").forEach(function (name) {
// engine lacks cannot throw at table-definition time. set[name] = 1;
var jsGlobals = { });
Object: function () { return Object; },
Boolean: function () { return Boolean; }, return set;
RegExp: function () { return RegExp; }, }
Promise: function () { return typeof Promise === "undefined" ? undefined : Promise; },
Set: function () { return typeof Set === "undefined" ? undefined : Set; }, /*
Map: function () { return typeof Map === "undefined" ? undefined : Map; }, * Called with window as the receiver. Detached, the browser throws
Error: function () { return Error; }, * "Illegal invocation" for these.
Symbol: function () { return typeof Symbol === "undefined" ? undefined : Symbol; }, */
BigInt: function () { return typeof BigInt === "undefined" ? undefined : BigInt; }, var boundWindowGlobals = nameSet(
Intl: function () { return typeof Intl === "undefined" ? undefined : Intl; }, "alert confirm prompt fetch print open scrollTo scrollBy matchMedia" +
parseInt: function () { return parseInt; }, " getComputedStyle structuredClone queueMicrotask btoa atob" +
parseFloat: function () { return parseFloat; }, " setTimeout clearTimeout setInterval clearInterval" +
isNaN: function () { return isNaN; }, " requestAnimationFrame cancelAnimationFrame",
isFinite: function () { return isFinite; }, );
encodeURIComponent: function () { return encodeURIComponent; },
decodeURIComponent: function () { return decodeURIComponent; }, /*
encodeURI: function () { return encodeURI; }, * Language builtins and other realm globals, read off globalThis. Naming
decodeURI: function () { return decodeURI; }, * them rather than referencing them directly means one an engine lacks
NaN: function () { return NaN; }, * resolves to undefined instead of throwing where the table is defined.
Infinity: function () { return Infinity; }, */
undefined: function () { return undefined; }, var ambientGlobals = nameSet(
}; "Object Boolean RegExp Promise Set Map Error Symbol BigInt Intl parseInt" +
" parseFloat isNaN isFinite encodeURIComponent decodeURIComponent" +
" encodeURI decodeURI NaN Infinity undefined Array Number String Math" +
" JSON Date URL",
);
/* /*
* toast(...) -- raise a notification from any client expression. * toast(...) -- raise a notification from any client expression.
@@ -154,27 +174,12 @@ export const REACTIVE_RUNTIME = String.raw`
if (!window.toast) window.toast = toastApi; if (!window.toast) window.toast = toastApi;
// Read straight off window, no binding needed (objects, not functions). // Read straight off window, no binding needed (objects, not functions).
var windowGlobals = { var windowGlobals = nameSet(
localStorage: 1, "localStorage sessionStorage screen performance crypto CustomEvent Event" +
sessionStorage: 1, " FormData URLSearchParams AbortController Notification" +
screen: 1, " IntersectionObserver ResizeObserver MutationObserver devicePixelRatio" +
performance: 1, " innerWidth innerHeight scrollX scrollY location history navigator",
crypto: 1, );
CustomEvent: 1,
Event: 1,
FormData: 1,
URLSearchParams: 1,
AbortController: 1,
Notification: 1,
IntersectionObserver: 1,
ResizeObserver: 1,
MutationObserver: 1,
devicePixelRatio: 1,
innerWidth: 1,
innerHeight: 1,
scrollX: 1,
scrollY: 1,
};
function reportDiagnostic(code, message, element, detail) { function reportDiagnostic(code, message, element, detail) {
var payload = { var payload = {
@@ -1071,7 +1076,7 @@ export const REACTIVE_RUNTIME = String.raw`
var serverProxy = new Proxy({}, { var serverProxy = new Proxy({}, {
get: function (_target, property) { get: function (_target, property) {
return function () { return function () {
return callServerFunction(componentRpcName, String(property), Array.prototype.slice.call(arguments)); return callServerFunction(componentRpcName, String(property), toArray(arguments));
}; };
}, },
}); });
@@ -1111,7 +1116,7 @@ export const REACTIVE_RUNTIME = String.raw`
if (name === "server") return serverProxy; if (name === "server") return serverProxy;
if (name === "props") return propsProxy; if (name === "props") return propsProxy;
if (name === "refs") return refsProxy; if (name === "refs") return refsProxy;
if (Object.prototype.hasOwnProperty.call(moduleBindings, name)) return moduleBindings[name]; if (hasOwn(moduleBindings, name)) return moduleBindings[name];
if (name === "$emit") { if (name === "$emit") {
return function (eventName, detail) { return function (eventName, detail) {
return dispatchComponentEvent(componentEventTarget, eventName, detail); return dispatchComponentEvent(componentEventTarget, eventName, detail);
@@ -1120,26 +1125,10 @@ export const REACTIVE_RUNTIME = String.raw`
if (name === "window") return window; if (name === "window") return window;
if (name === "document") return document; if (name === "document") return document;
if (name === "console") return console; if (name === "console") return console;
if (name === "Array") return Array;
if (name === "Number") return Number;
if (name === "String") return String;
if (name === "Math") return Math;
if (name === "JSON") return JSON;
if (name === "Date") return Date;
if (name === "URL") return URL;
if (name === "location") return window.location;
if (name === "history") return window.history;
if (name === "navigator") return window.navigator;
if (name === "$route" || name === "route") { if (name === "$route" || name === "route") {
if (currentRenderer) routeValue.subscribe(currentRenderer); if (currentRenderer) routeValue.subscribe(currentRenderer);
return routeValue.get(); return routeValue.get();
} }
if (name === "setTimeout") return window.setTimeout.bind(window);
if (name === "clearTimeout") return window.clearTimeout.bind(window);
if (name === "setInterval") return window.setInterval.bind(window);
if (name === "clearInterval") return window.clearInterval.bind(window);
if (name === "requestAnimationFrame") return window.requestAnimationFrame.bind(window);
if (name === "cancelAnimationFrame") return window.cancelAnimationFrame.bind(window);
/* /*
* Ordinary browser and language globals. * Ordinary browser and language globals.
* *
@@ -1156,11 +1145,11 @@ export const REACTIVE_RUNTIME = String.raw`
* lacks one of these does not break the rest. * lacks one of these does not break the rest.
*/ */
if (name === "toast") return toastApi; if (name === "toast") return toastApi;
if (dialogGlobals[name] && typeof window[name] === "function") { if (boundWindowGlobals[name] && typeof window[name] === "function") {
return window[name].bind(window); return window[name].bind(window);
} }
if (jsGlobals[name]) { if (ambientGlobals[name]) {
var builtin = jsGlobals[name](); var builtin = globalThis[name];
if (builtin !== undefined) return builtin; if (builtin !== undefined) return builtin;
} }
if (windowGlobals[name]) { if (windowGlobals[name]) {
@@ -1174,7 +1163,7 @@ export const REACTIVE_RUNTIME = String.raw`
} }
function readScope(name) { function readScope(name) {
if (Object.prototype.hasOwnProperty.call(computedDefinitions, name)) { if (hasOwn(computedDefinitions, name)) {
if (computing.has(name)) { if (computing.has(name)) {
reportDiagnostic("WRN-COMPUTED-CYCLE", "Computed value '" + name + "' has a dependency cycle.", el); reportDiagnostic("WRN-COMPUTED-CYCLE", "Computed value '" + name + "' has a dependency cycle.", el);
return undefined; return undefined;
@@ -1191,18 +1180,18 @@ export const REACTIVE_RUNTIME = String.raw`
if (currentRenderer) sig.subscribe(currentRenderer); if (currentRenderer) sig.subscribe(currentRenderer);
return sig.get(); return sig.get();
} }
if (Object.prototype.hasOwnProperty.call(behaviorFunctions, name)) { if (hasOwn(behaviorFunctions, name)) {
return behaviorFunctions[name]; return behaviorFunctions[name];
} }
return readGlobal(name); return readGlobal(name);
} }
function peekScope(name) { function peekScope(name) {
if (Object.prototype.hasOwnProperty.call(computedDefinitions, name)) { if (hasOwn(computedDefinitions, name)) {
return readScope(name); return readScope(name);
} }
if (signals[name]) return signals[name].get(); if (signals[name]) return signals[name].get();
if (Object.prototype.hasOwnProperty.call(behaviorFunctions, name)) { if (hasOwn(behaviorFunctions, name)) {
return behaviorFunctions[name]; return behaviorFunctions[name];
} }
return readGlobal(name); return readGlobal(name);
@@ -1265,7 +1254,7 @@ export const REACTIVE_RUNTIME = String.raw`
function evalExpr(expr, locals) { function evalExpr(expr, locals) {
return evaluateExpression(expr, function (name) { return evaluateExpression(expr, function (name) {
if (locals && Object.prototype.hasOwnProperty.call(locals, name)) { if (locals && hasOwn(locals, name)) {
return locals[name]; return locals[name];
} }
return readScope(name); return readScope(name);
@@ -1299,8 +1288,7 @@ export const REACTIVE_RUNTIME = String.raw`
function (name) { function (name) {
if ( if (
locals && locals &&
Object.prototype hasOwn(
.hasOwnProperty.call(
locals, locals,
name, name,
) )
@@ -1313,8 +1301,7 @@ export const REACTIVE_RUNTIME = String.raw`
function (name, value) { function (name, value) {
if ( if (
locals && locals &&
Object.prototype hasOwn(
.hasOwnProperty.call(
locals, locals,
name, name,
) )
@@ -1506,19 +1493,13 @@ export const REACTIVE_RUNTIME = String.raw`
} }
return type + ":" + String(value); return type + ":" + String(value);
} }
function fillMustache(str, itemEval) {
return str.replace(/\{\{\s*([^}]+?)\s*\}\}|\{([^{}]+)\}/g, function (_, d, s) {
var e = (d || s).trim();
try { return String(itemEval(e)); } catch (err) { return ""; }
});
}
function hydrateItem( function hydrateItem(
root, root,
locals, locals,
) { ) {
function localRead(name) { function localRead(name) {
if ( if (
Object.prototype.hasOwnProperty.call( hasOwn(
locals, locals,
name, name,
) )
@@ -1602,7 +1583,7 @@ export const REACTIVE_RUNTIME = String.raw`
if (node !== root && insideNestedLoop(node)) return; if (node !== root && insideNestedLoop(node)) return;
var attributes = var attributes =
Array.prototype.slice.call( toArray(
node.attributes, node.attributes,
); );
@@ -1644,28 +1625,12 @@ export const REACTIVE_RUNTIME = String.raw`
"data-wrn-class-", "data-wrn-class-",
) === 0 ) === 0
) { ) {
var classBinding; var classBinding = pairBinding(attribute.value);
try { if (!classBinding) return;
classBinding = JSON.parse(
attribute.value,
);
} catch (_) {
return;
}
if ( var className = classBinding[0];
!classBinding || var classExpression = classBinding[1];
classBinding.length !== 2
) {
return;
}
var className =
classBinding[0];
var classExpression =
classBinding[1];
var classEnabled = false; var classEnabled = false;
@@ -1695,28 +1660,13 @@ export const REACTIVE_RUNTIME = String.raw`
) === 0 ) === 0
) { ) {
node.removeAttribute(attribute.name); node.removeAttribute(attribute.name);
var binding;
try { var binding = pairBinding(attribute.value);
binding = JSON.parse(
attribute.value,
);
} catch (_) {
return;
}
if ( if (!binding) return;
!binding ||
binding.length !== 2
) {
return;
}
var attributeName = var attributeName = binding[0];
binding[0]; var attributeTemplate = binding[1];
var attributeTemplate =
binding[1];
/* /*
* Reactive, not resolved once. The expression can read component * Reactive, not resolved once. The expression can read component
@@ -1784,7 +1734,7 @@ export const REACTIVE_RUNTIME = String.raw`
eventLocals.event = event; eventLocals.event = event;
eventLocals.$event = event; eventLocals.$event = event;
eventLocals.payload = event && Object.prototype.hasOwnProperty.call(event, "detail") ? event.detail : undefined; eventLocals.payload = event && hasOwn(event, "detail") ? event.detail : undefined;
try { try {
runStmt( runStmt(
@@ -1936,8 +1886,7 @@ export const REACTIVE_RUNTIME = String.raw`
// Hand every nested loop its own renderer, with this item in scope. // Hand every nested loop its own renderer, with this item in scope.
if (root.querySelectorAll) { if (root.querySelectorAll) {
Array.prototype.slice toArray(root.querySelectorAll("[data-for]"))
.call(root.querySelectorAll("[data-for]"))
.forEach(function (nested) { .forEach(function (nested) {
// Only the outermost nested templates: deeper ones are set up by // Only the outermost nested templates: deeper ones are set up by
// their own parent when it renders. // their own parent when it renders.
@@ -2011,12 +1960,12 @@ export const REACTIVE_RUNTIME = String.raw`
var firstRun = !outerLocals; var firstRun = !outerLocals;
function controlRead(name) { function controlRead(name) {
return Object.prototype.hasOwnProperty.call(inherited, name) ? inherited[name] : readScope(name); return hasOwn(inherited, name) ? inherited[name] : readScope(name);
} }
function controlEval(expression, locals) { function controlEval(expression, locals) {
return evaluateExpression(expression, function (name) { return evaluateExpression(expression, function (name) {
return locals && Object.prototype.hasOwnProperty.call(locals, name) return locals && hasOwn(locals, name)
? locals[name] ? locals[name]
: controlRead(name); : controlRead(name);
}); });
@@ -2033,7 +1982,7 @@ export const REACTIVE_RUNTIME = String.raw`
var template = document.createElement("template"); var template = document.createElement("template");
template.innerHTML = markup || ""; template.innerHTML = markup || "";
var fragment = template.content; var fragment = template.content;
var elements = Array.prototype.slice.call(fragment.childNodes).filter(function (node) { var elements = toArray(fragment.childNodes).filter(function (node) {
return node.nodeType === 1; return node.nodeType === 1;
}); });
if (rangeEnd) block.parentNode.insertBefore(fragment, rangeEnd); if (rangeEnd) block.parentNode.insertBefore(fragment, rangeEnd);
@@ -2100,7 +2049,7 @@ export const REACTIVE_RUNTIME = String.raw`
}); });
} }
Array.prototype.slice.call(el.querySelectorAll("[data-wrn-if],[data-wrn-each]")).forEach(function (block) { toArray(el.querySelectorAll("[data-wrn-if],[data-wrn-each]")).forEach(function (block) {
if (block.parentElement && block.parentElement.closest("[data-wrn-if],[data-wrn-each]")) return; if (block.parentElement && block.parentElement.closest("[data-wrn-if],[data-wrn-each]")) return;
setupControlBlock(block, null); setupControlBlock(block, null);
}); });
@@ -2132,7 +2081,7 @@ export const REACTIVE_RUNTIME = String.raw`
} }
function loopRead(name) { function loopRead(name) {
if (Object.prototype.hasOwnProperty.call(inherited, name)) { if (hasOwn(inherited, name)) {
return inherited[name]; return inherited[name];
} }
return readScope(name); return readScope(name);
@@ -2305,7 +2254,7 @@ export const REACTIVE_RUNTIME = String.raw`
rawKey = evaluateExpression( rawKey = evaluateExpression(
keyExpression, keyExpression,
function (name) { function (name) {
return Object.prototype.hasOwnProperty.call(keyedLocals, name) return hasOwn(keyedLocals, name)
? keyedLocals[name] ? keyedLocals[name]
: loopRead(name); : loopRead(name);
}, },
@@ -2378,8 +2327,7 @@ export const REACTIVE_RUNTIME = String.raw`
} }
Array.prototype.slice toArray(el.querySelectorAll("[data-for]"))
.call(el.querySelectorAll("[data-for]"))
.forEach(function (tpl) { .forEach(function (tpl) {
// Only top-level templates here; nested ones are connected by the item // Only top-level templates here; nested ones are connected by the item
// that contains them, once it has values to give them. // that contains them, once it has values to give them.
@@ -2475,24 +2423,18 @@ export const REACTIVE_RUNTIME = String.raw`
// Conditional class bindings emitted as: // Conditional class bindings emitted as:
// data-wrn-class-*='["class-name","expression"]' // data-wrn-class-*='["class-name","expression"]'
var classBindNodes = [el].concat( var classBindNodes = [el].concat(
Array.prototype.slice.call(el.querySelectorAll("*")), toArray(el.querySelectorAll("*")),
); );
classBindNodes.forEach(function (node) { classBindNodes.forEach(function (node) {
if (!owns(node)) return; if (!owns(node)) return;
Array.prototype.slice.call(node.attributes).forEach(function (marker) { toArray(node.attributes).forEach(function (marker) {
if (marker.name.indexOf("data-wrn-class-") !== 0) return; if (marker.name.indexOf("data-wrn-class-") !== 0) return;
var binding; var binding = pairBinding(marker.value);
try { if (!binding) return;
binding = JSON.parse(marker.value);
} catch (e) {
return;
}
if (!binding || binding.length !== 2) return;
var className = binding[0]; var className = binding[0];
var expression = binding[1]; var expression = binding[1];
@@ -2521,7 +2463,7 @@ export const REACTIVE_RUNTIME = String.raw`
// [attributeName, originalTemplate], preserving an SSR value while allowing // [attributeName, originalTemplate], preserving an SSR value while allowing
// state changes to update type, aria-*, class, href, and other attributes. // state changes to update type, aria-*, class, href, and other attributes.
var bindNodes = [el].concat( var bindNodes = [el].concat(
Array.prototype.slice.call( toArray(
el.querySelectorAll("*"), el.querySelectorAll("*"),
), ),
); );
@@ -2529,8 +2471,7 @@ export const REACTIVE_RUNTIME = String.raw`
bindNodes.forEach(function (node) { bindNodes.forEach(function (node) {
if (!owns(node)) return; if (!owns(node)) return;
Array.prototype.slice toArray(node.attributes)
.call(node.attributes)
.forEach(function (marker) { .forEach(function (marker) {
if ( if (
marker.name.indexOf( marker.name.indexOf(
@@ -2541,22 +2482,10 @@ export const REACTIVE_RUNTIME = String.raw`
} }
node.removeAttribute(marker.name); node.removeAttribute(marker.name);
var binding;
try { var binding = pairBinding(marker.value);
binding = JSON.parse(
marker.value,
);
} catch (error) {
return;
}
if ( if (!binding) return;
!binding ||
binding.length !== 2
) {
return;
}
var name = binding[0]; var name = binding[0];
var template = binding[1]; var template = binding[1];
@@ -2648,10 +2577,10 @@ export const REACTIVE_RUNTIME = String.raw`
} }
// Event handlers on elements, window, and document. // Event handlers on elements, window, and document.
var nodes = [el].concat(Array.prototype.slice.call(el.querySelectorAll("*"))); var nodes = [el].concat(toArray(el.querySelectorAll("*")));
nodes.forEach(function (node) { nodes.forEach(function (node) {
if (!owns(node)) return; if (!owns(node)) return;
Array.prototype.slice.call(node.attributes).forEach(function (attr) { toArray(node.attributes).forEach(function (attr) {
if (attr.name.indexOf("data-on-") !== 0) return; if (attr.name.indexOf("data-on-") !== 0) return;
var rawName = attr.name.slice("data-on-".length); var rawName = attr.name.slice("data-on-".length);
@@ -2678,7 +2607,7 @@ export const REACTIVE_RUNTIME = String.raw`
locals.event = event; locals.event = event;
locals.$event = event; locals.$event = event;
locals.payload = event && Object.prototype.hasOwnProperty.call(event, "detail") ? event.detail : undefined; locals.payload = event && hasOwn(event, "detail") ? event.detail : undefined;
try { try {
runStmt( runStmt(
@@ -2727,8 +2656,7 @@ export const REACTIVE_RUNTIME = String.raw`
// function only exists out here. The compiler emits these as data-wrn-out-* so the // function only exists out here. The compiler emits these as data-wrn-out-* so the
// two cases stay distinguishable, and this scope claims every one that // two cases stay distinguishable, and this scope claims every one that
// sits on a component it directly mounts. // sits on a component it directly mounts.
Array.prototype.slice toArray(el.querySelectorAll("[data-wrn-events]"))
.call(el.querySelectorAll("[data-wrn-events]"))
.forEach(function (node) { .forEach(function (node) {
var componentRoot = closestScope(node); var componentRoot = closestScope(node);
if (!componentRoot || componentRoot === el) return; if (!componentRoot || componentRoot === el) return;
@@ -2745,7 +2673,7 @@ export const REACTIVE_RUNTIME = String.raw`
node.__wrnexusOutputHandlers || node.__wrnexusOutputHandlers ||
(node.__wrnexusOutputHandlers = {}); (node.__wrnexusOutputHandlers = {});
Array.prototype.slice.call(node.attributes).forEach(function (attr) { toArray(node.attributes).forEach(function (attr) {
if (attr.name.indexOf("data-wrn-out-") !== 0) return; if (attr.name.indexOf("data-wrn-out-") !== 0) return;
var outName = attr.name.slice("data-wrn-out-".length); var outName = attr.name.slice("data-wrn-out-".length);
@@ -2783,7 +2711,7 @@ export const REACTIVE_RUNTIME = String.raw`
locals.event = event; locals.event = event;
locals.$event = event; locals.$event = event;
locals.payload = locals.payload =
event && Object.prototype.hasOwnProperty.call(event, "detail") event && hasOwn(event, "detail")
? event.detail ? event.detail
: undefined; : undefined;
try { try {
@@ -2805,16 +2733,14 @@ export const REACTIVE_RUNTIME = String.raw`
// Prop expressions belong to the parent that mounted the component. The // Prop expressions belong to the parent that mounted the component. The
// server forwards these markers onto the rendered child root; evaluate // server forwards these markers onto the rendered child root; evaluate
// them here and write changes into the child's prop signals. // them here and write changes into the child's prop signals.
Array.prototype.slice toArray(el.querySelectorAll("*"))
.call(el.querySelectorAll("*"))
.filter(isScopeRoot) .filter(isScopeRoot)
.forEach(function (node) { .forEach(function (node) {
if (!node.parentNode || ownerScope(node.parentNode) !== el) return; if (!node.parentNode || ownerScope(node.parentNode) !== el) return;
Array.prototype.slice.call(node.attributes).forEach(function (attr) { toArray(node.attributes).forEach(function (attr) {
if (attr.name.indexOf("data-wrn-prop-bind-") !== 0) return; if (attr.name.indexOf("data-wrn-prop-bind-") !== 0) return;
var binding; var binding = pairBinding(attr.value);
try { binding = JSON.parse(attr.value); } catch (_) { return; } if (!binding) return;
if (!binding || binding.length !== 2) return;
var propName = binding[0]; var propName = binding[0];
var template = binding[1]; var template = binding[1];
reactive(function () { reactive(function () {
@@ -3168,8 +3094,7 @@ export const REACTIVE_RUNTIME = String.raw`
var host = root && root.querySelectorAll ? root : document; var host = root && root.querySelectorAll ? root : document;
anchoredWriting = true; anchoredWriting = true;
try { try {
Array.prototype.slice toArray(host.querySelectorAll(ANCHORED_SELECTOR))
.call(host.querySelectorAll(ANCHORED_SELECTOR))
.forEach(clampAnchored); .forEach(clampAnchored);
} finally { } finally {
// Released on a timer, not requestAnimationFrame. rAF does not fire in // Released on a timer, not requestAnimationFrame. rAF does not fire in
@@ -3703,8 +3628,7 @@ export const REACTIVE_RUNTIME = String.raw`
// therefore no client-side binding to retain; consume its compiler markers // therefore no client-side binding to retain; consume its compiler markers
// separately from component hydration. // separately from component hydration.
if (host === document || host === document.documentElement) { if (host === document || host === document.documentElement) {
Array.prototype.slice toArray(document.documentElement.attributes)
.call(document.documentElement.attributes)
.forEach(function (attribute) { .forEach(function (attribute) {
if (attribute.name.indexOf("data-wrn-bind-") === 0) { if (attribute.name.indexOf("data-wrn-bind-") === 0) {
document.documentElement.removeAttribute(attribute.name); document.documentElement.removeAttribute(attribute.name);
@@ -4346,7 +4270,7 @@ export const REACTIVE_RUNTIME = String.raw`
function emitPinInputEvent(root, name, extra) { function emitPinInputEvent(root, name, extra) {
var hidden = root.querySelector("[data-pin-value]"); var hidden = root.querySelector("[data-pin-value]");
var cells = Array.prototype.slice.call(root.querySelectorAll("[data-pin-cell]")); var cells = toArray(root.querySelectorAll("[data-pin-cell]"));
var value = hidden ? hidden.value : ""; var value = hidden ? hidden.value : "";
var detail = { var detail = {
component: "PinInput", component: "PinInput",
@@ -4368,7 +4292,7 @@ export const REACTIVE_RUNTIME = String.raw`
/*__WRNEXUS_CONTROLLERS_PIN_START__*/ /*__WRNEXUS_CONTROLLERS_PIN_START__*/
function setupPinInputController(root) { function setupPinInputController(root) {
if (!root || root.__wrnexusPinInputController) return; if (!root || root.__wrnexusPinInputController) return;
var cells = Array.prototype.slice.call(root.querySelectorAll("[data-pin-cell]")); var cells = toArray(root.querySelectorAll("[data-pin-cell]"));
var hidden = root.querySelector("[data-pin-value]"); var hidden = root.querySelector("[data-pin-value]");
var clearButton = root.querySelector("[data-pin-clear]"); var clearButton = root.querySelector("[data-pin-clear]");
var patternSource = root.getAttribute("data-pattern") || "[0-9]"; var patternSource = root.getAttribute("data-pattern") || "[0-9]";
@@ -5960,7 +5884,7 @@ export const REACTIVE_RUNTIME = String.raw`
host.querySelectorAll("[data-wrn-dynamic-component]").forEach(function (element) { host.querySelectorAll("[data-wrn-dynamic-component]").forEach(function (element) {
if (element.__wrnDynamicMounted) return; if (element.__wrnDynamicMounted) return;
element.__wrnDynamicMounted = true; element.__wrnDynamicMounted = true;
var cases = Array.prototype.slice.call(element.children).filter(function (candidate) { var cases = toArray(element.children).filter(function (candidate) {
return candidate.hasAttribute("data-component-case"); return candidate.hasAttribute("data-component-case");
}).map(function (candidate) { }).map(function (candidate) {
var marker = document.createComment("wrnexus-component-case:" + (candidate.getAttribute("data-component-case") || "")); var marker = document.createComment("wrnexus-component-case:" + (candidate.getAttribute("data-component-case") || ""));
+28
View File
@@ -0,0 +1,28 @@
import { afterAll } from "bun:test";
/**
* Restore globals a suite replaces, once the suite is done.
*
* These suites install a happy-dom window over the real globals and delete
* them before each test so every test starts clean. bun test loads and runs
* one file at a time rather than importing them all up front, so anything left
* deleted is still missing when the next suite runs -- which is how `bun test`
* with no argument came to fail unrelated files with "fetch is not a
* function". Names absent at capture time are deleted again rather than being
* restored as undefined, so a global that never existed does not gain a key.
*/
export function restoreGlobalsAfterAll(names: readonly string[]): void {
const captured = new Map<string, unknown>(
names.map((name) => [name, (globalThis as Record<string, unknown>)[name]]),
);
afterAll(() => {
for (const [name, value] of captured) {
if (value === undefined) {
delete (globalThis as Record<string, unknown>)[name];
} else {
(globalThis as Record<string, unknown>)[name] = value;
}
}
});
}
+15 -10
View File
@@ -1,6 +1,7 @@
import { test, expect, beforeEach } from "bun:test"; import { test, expect, beforeEach } from "bun:test";
import { Window } from "happy-dom"; import { Window } from "happy-dom";
import { NAV_RUNTIME } from "../src/nav-runtime.ts"; import { NAV_RUNTIME } from "../src/nav-runtime.ts";
import { restoreGlobalsAfterAll } from "./global-restore.ts";
let win: any; let win: any;
let fetchCalls: { url: string; opts: any }[]; let fetchCalls: { url: string; opts: any }[];
@@ -36,18 +37,22 @@ function install(bodyHtml: string): void {
const flush = () => new Promise((r) => setTimeout(r, 0)); const flush = () => new Promise((r) => setTimeout(r, 0));
const REPLACED_GLOBALS = [
"window",
"document",
"history",
"location",
"DOMParser",
"CustomEvent",
"Event",
"fetch",
];
restoreGlobalsAfterAll(REPLACED_GLOBALS);
beforeEach(() => { beforeEach(() => {
const g = globalThis as any; const g = globalThis as any;
for (const k of [ for (const k of REPLACED_GLOBALS) {
"window",
"document",
"history",
"location",
"DOMParser",
"CustomEvent",
"Event",
"fetch",
]) {
delete g[k]; delete g[k];
} }
}); });
+28 -5
View File
@@ -3,6 +3,7 @@ import { Window } from "happy-dom";
import { REACTIVE_RUNTIME } from "../src/reactive-runtime.ts"; import { REACTIVE_RUNTIME } from "../src/reactive-runtime.ts";
import { getComponentControllerRuntime, getReactiveRuntime } from "../src/index.ts"; import { getComponentControllerRuntime, getReactiveRuntime } from "../src/index.ts";
import { mountHtml } from "@wrnexus/test"; import { mountHtml } from "@wrnexus/test";
import { restoreGlobalsAfterAll } from "./global-restore.ts";
// Fresh DOM per test, with the runtime's globals bound. // Fresh DOM per test, with the runtime's globals bound.
function mount(html: string, runtime = REACTIVE_RUNTIME, controllers = ""): Window { function mount(html: string, runtime = REACTIVE_RUNTIME, controllers = ""): Window {
@@ -34,12 +35,14 @@ function mount(html: string, runtime = REACTIVE_RUNTIME, controllers = ""): Wind
return win as unknown as Window; return win as unknown as Window;
} }
const REPLACED_GLOBALS = ["window", "document", "location", "fetch", "MutationObserver"];
restoreGlobalsAfterAll(REPLACED_GLOBALS);
beforeEach(() => { beforeEach(() => {
delete (globalThis as Record<string, unknown>).window; for (const name of REPLACED_GLOBALS) {
delete (globalThis as Record<string, unknown>).document; delete (globalThis as Record<string, unknown>)[name];
delete (globalThis as Record<string, unknown>).location; }
delete (globalThis as Record<string, unknown>).fetch;
delete (globalThis as Record<string, unknown>).MutationObserver;
}); });
test("split runtime hydrates a controller only from the controller asset", () => { test("split runtime hydrates a controller only from the controller asset", () => {
@@ -1758,3 +1761,23 @@ test("an unbounded loop stops instead of hanging the page", () => {
expect(value).toBeGreaterThan(0); expect(value).toBeGreaterThan(0);
expect(Number.isFinite(value)).toBe(true); expect(Number.isFinite(value)).toBe(true);
}); });
test("a class binding inside data-for follows state the row never mentions", () => {
// The row's own array is untouched, so nothing rebuilds the list. The
// binding has to be reactive in its own right to keep up.
const binding = JSON.stringify(["is-active", "selected === row.id"]);
const win = mount(
`<div data-scope="rows: [{&quot;id&quot;:1},{&quot;id&quot;:2}], selected: 1">` +
`<button data-on-click="selected = 2">pick</button>` +
`<ul><li data-for="row in rows" data-wrn-class-active='${binding}'></li></ul>` +
`</div>`,
);
const items = () => Array.from(win.document.querySelectorAll("li"));
expect(items()[0]?.classList.contains("is-active")).toBe(true);
expect(items()[1]?.classList.contains("is-active")).toBe(false);
win.document.querySelector("button")!.click();
expect(items()[0]?.classList.contains("is-active")).toBe(false);
expect(items()[1]?.classList.contains("is-active")).toBe(true);
});
+6 -1
View File
@@ -1,6 +1,7 @@
import { test, expect, beforeEach } from "bun:test"; import { test, expect, beforeEach } from "bun:test";
import { Window } from "happy-dom"; import { Window } from "happy-dom";
import { REALTIME_RUNTIME } from "../src/realtime-runtime.ts"; import { REALTIME_RUNTIME } from "../src/realtime-runtime.ts";
import { restoreGlobalsAfterAll } from "./global-restore.ts";
/* A fake WebSocket that records instances + sent frames and lets tests drive events. */ /* A fake WebSocket that records instances + sent frames and lets tests drive events. */
let sockets: FakeWS[]; let sockets: FakeWS[];
@@ -45,8 +46,12 @@ function boot(bodyHtml: string) {
return win as unknown as Window; return win as unknown as Window;
} }
const REPLACED_GLOBALS = ["window", "document", "location", "WebSocket"];
restoreGlobalsAfterAll(REPLACED_GLOBALS);
beforeEach(() => { beforeEach(() => {
for (const k of ["window", "document", "location", "WebSocket"]) { for (const k of REPLACED_GLOBALS) {
delete (globalThis as Record<string, unknown>)[k]; delete (globalThis as Record<string, unknown>)[k];
} }
}); });
+15 -5
View File
@@ -1654,11 +1654,21 @@ test("carousel supports RTL, multiple slides, dragging, snap, and thumbnail layo
expect(css).toContain('.wrn-next--carousel[data-centered="true"] .wrn-next__carousel-track'); expect(css).toContain('.wrn-next--carousel[data-centered="true"] .wrn-next__carousel-track');
}); });
test("carousel autoplay timers are available in the browser reactive runtime", async () => { test("carousel autoplay timers are available in the browser reactive runtime", () => {
const { getReactiveRuntime } = await import("../../csr/src/index.ts"); // Resolve them through the runtime rather than asserting on its source: the
const runtime = getReactiveRuntime(); // timers only have to be reachable from a client expression, and a substring
expect(runtime).toContain('name === "setInterval"'); // check goes stale the moment the lookup is written differently.
expect(runtime).toContain('name === "clearInterval"'); const dom = mountHtml(
`<div data-scope="started: 0, stopped: 0">` +
`<button data-on-click="started = setInterval; stopped = clearInterval">go</button>` +
`<span class="started" data-text="started"></span>` +
`<span class="stopped" data-text="stopped"></span>` +
`</div>`,
);
(dom.querySelector("button") as HTMLButtonElement).click();
expect(dom.querySelector(".started")?.textContent).toContain("function");
expect(dom.querySelector(".stopped")?.textContent).toContain("function");
}); });
test("carousel snap controls scroll and multiple slides stop at the last full group", async () => { test("carousel snap controls scroll and multiple slides stop at the last full group", async () => {
+11 -6
View File
@@ -170,13 +170,18 @@ addCheck(
*/ */
const runtimeBudgets = { const runtimeBudgets = {
/* /*
* Raised from 49,000 on 2026-08-19. The reactive runtime had already grown * Raised from 49,000 on 2026-08-19, to just above what the runtime actually
* past the old figure, and client-side control blocks ({#if}/{#each} * minifies to rather than to a round number with room to drift.
* rerendering, for/while in handlers) added the rest. What a visitor pays is *
* the compressed transfer: 52,570 minified is 16,803 gzipped, once, behind * The runtime was already over 49,000 before client-side control blocks and
* an immutable year-long cache. * for/while support were added. Trimming it afterwards -- prototype-safe
* global lookup tables, shared hasOwn/toArray/pairBinding helpers, dead code
* -- recovered 2,414 bytes, which was everything available without dropping
* or deferring a feature. What a visitor pays is the compressed transfer:
* 50,156 minified is ~16,000 gzipped, once, behind an immutable year-long
* cache.
*/ */
"reactive-runtime.ts": 53_000, "reactive-runtime.ts": 50_500,
"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,