Repository navigation
Scripting
Pages run their scripts. A JavaScript engine of our own (src/js, see Architecture) is connected to the document through the bindings in src/bindings — the HTML element interfaces, their tags and every attribute reflected from a content attribute are generated from idl/html-elements.idl by tools/gen-bindings.cpp (a WebIDL subset: interfaces with parents, [Tags], [Reflect], [URL], [Default]; the output is committed and a test holds it to the IDL), and the accessors and methods that read live state are written by hand — and every host — the window, --render, the WPT runner and the shell's --script replay — drives the same realm the same way.
The parser hands each <script> element over as its end tag goes by, with the parser paused, as the HTML specification asks: an inline classic script runs at once, an external one is fetched through the page's own session and runs at once, and one marked defer or async runs after the parse, in order, before DOMContentLoaded. A type="module" script is fetched with its whole import graph — in CORS mode, with the credentials its crossorigin attribute asks for, each response held to a JavaScript type — and runs after the parse in document order with the deferred classic scripts, or at once when marked async or inserted by a script; an inline module has the document's URL as its base and as its import.meta.url, module code is strict with no this and no document.currentScript, a module named twice is fetched and evaluated once, a failure anywhere in a graph is the element's error event and a console line, and import() from any script resolves against the document on the same module map. Data blocks (JSON, templates, import maps) are skipped by name. <noscript> content is raw text, since scripting is on. A <script> element a script inserts into the document runs; the scripts inside markup set through innerHTML never do, as the specification says. document.write while the parser runs puts its text at the insertion point, one write after another in order, so a script it writes — inline or external — runs when the parser reaches it and sees only what came before it, and a script that a written script writes lands after that script's own end tag; a write after the parse is reported to the console rather than replacing the page, and document.open() answers with the document and replaces nothing.
The document's Content Security Policy (Security defaults) has its say before any of that runs: an inline classic or module script, and an event handler attribute, is held to the governing script-src directive — 'unsafe-inline', its nonce attribute, or a hash of its text in SHA-256, -384 or -512 ('unsafe-hashes' for a handler attribute) — and one refused never runs; an external script's URL is judged with the element's nonce, and under 'strict-dynamic' a script that a trusted script inserted loads where a parser-inserted one needs its nonce; eval, Function and its kin, and a string given to setTimeout, throw an EvalError unless 'unsafe-eval' is allowed; a sandbox directive without allow-scripts runs no script at all, and the page parses with scripting off so its <noscript> content shows. A <meta http-equiv="content-security-policy"> in the head is enforced from the moment it appears, for what follows it. Every refusal is a console line, once.
When the parse is done, readyState moves to interactive, the deferred scripts run, DOMContentLoaded fires on the document, each iframe the parse left in the tree has its document opened in a realm of its own and fires load, readyState moves to complete, and load fires on the window. A frame's realm shares the page's agent — one heap, one microtask queue, one event loop — and has its own global, its own intrinsics and its own document; a script the page holds of the frame's, or the frame of the page's, runs in the realm it was made in. A script reaches every window through its WindowProxy, which stays the same while the frame moves from document to document and shows a window of another origin only what HTML lets it see (document.domain relaxes that between documents that set the same domain), and a message posted between two windows arrives as a task with the sender's WindowProxy as its source and a structured clone of the value, made in the realm that receives it. An iframe with nothing to show — no src, an empty one, about:blank — has the initial about:blank document, empty and of its parent's origin, with a window of its own the moment it is in the tree; an iframe a script inserts, or whose src or srcdoc a script changes, navigates in a task after that script, firing load at the iframe once; a frame's own Location navigates it the same way, a change of the fragment alone firing hashchange instead, and a javascript: URL runs in the frame's document when the document that asked has its origin; the iframe's sandbox attribute is read when the frame navigates — no script without allow-scripts, an opaque origin without allow-same-origin, no navigating the top without allow-top-navigation, and never document.domain; an iframe taken out of its tree closes its frame at once and ends its realm once the host's entry has returned, so a window a script still holds answers from an empty document rather than from freed memory (Not-written-yet has what a frame does not do yet). Timers then run on the host's clock — wall time in the window, a virtual clock in the renderer and the runners — and every entry from the host into script ends with a microtask checkpoint, which runs the promise reactions and queueMicrotask callbacks queued so far, in one order, until none is left.
-
The tree —
Node,Element,Document,Text,Comment,DocumentFragment,DocumentType; the HTML element interfaces fromHTMLAnchorElementtoHTMLVideoElementwith their attributes reflected (hrefresolved against the page,valueandcheckedlive from the shell's form state,selectedIndex,options,elements,rows,cells…);querySelectorandquerySelectorAllover the engine's own selector matcher,getElementById,getElementsByClassName,getElementsByTagName,closest,matches;appendChild,insertBefore,replaceChild,removeChild,append,prepend,before,after,replaceWith,cloneNode,insertAdjacentHTML;innerHTMLandouterHTMLthrough a fragment serializer and the fragment parser;textContent;classList,dataset,attributes,getAttributeand the rest;DOMParser,document.implementation.createHTMLDocument. -
Style and geometry —
element.stylereads and writes the style attribute declaration by declaration;getComputedStyleanswers from the computed style;getBoundingClientRect,offsetWidthand the other box measures come from a layout the host runs on demand, so a script that measures and then sets a width gets the number the page would have. -
Events — the dispatch algorithm with capture, target and bubble phases,
addEventListeneroptions (capture,once,passive),on<type>handlers set as properties or content attributes (areturn falsecancels),Event,CustomEvent,UIEvent,MouseEvent,KeyboardEvent,InputEvent,FocusEvent,WheelEvent,PopStateEvent; the shell firesmousedown,click,keydownandinputat the element under the pointer or in focus, andpreventDefaultkeeps a link from being followed or a control from toggling. -
Binary data —
ArrayBuffer(fixed or resizable,slice,transfer), the typed arrays fromInt8ArraytoFloat64ArraywithFloat16Array, andDataView, all the engine's own (Architecture); on the page sideTextEncoderandTextDecoder(UTF-8 with its streaming and fatal modes, UTF-16, windows-1252),BlobandFilewithslice,text,arrayBufferandbytes, andcrypto.getRandomValuesover any integer view. -
The network —
fetchwithHeaders,Request,ResponseandFormData(bodies as strings,Blobs, buffers,FormDataas multipart,URLSearchParamsas urlencoded;text,json,arrayBuffer,blob,bytes,formDatato read one),XMLHttpRequestwith its five states, its events andresponseType,AbortController/AbortSignal,MessageChannel/MessagePortandwindow.postMessagebetween windows, each message a structured clone with theArrayBuffers andMessagePorts its transfer list names moved rather than copied. A request goes through the shell's own loader with the page's session — cookies only where the request allows them — and the browser's rules apply: a cross-origin response reaches the page only when itsAccess-Control-Allow-Originsays so, a request that is not a simple one sends a preflight first,no-corsgets an opaque response. The round trip runs on the event loop's next task, synchronously inside it, until the process split; a response never settles a promise or fires an event in the same turn that asked for it. -
The window —
setTimeout,setInterval,requestAnimationFrame,requestIdleCallback,queueMicrotask;location(assigning it navigates),history(pushStatemoves the URL),navigator,screen,innerWidth,scrollTo,matchMediaover the engine's media queries,localStorageandsessionStorage(in memory),document.cookie(through the session's jar in the window),URLandURLSearchParams,Origin,DOMException,atob/btoa,structuredClone,performance.now,console,alert(logged),IntersectionObserverandResizeObserver(each observed target reported once, in view),MutationObserver(constructible; records are not written),customElements.define(recorded, not upgraded); and named access, as HTML has it — an HTML element'sid, or thenameof anembed,form,imgorobject, is a property of the window (one element as itself, several as anHTMLCollectionin tree order), behind the window's own members,Object.prototype's and every global a script declares, and read from the tree as it stands at each lookup.
An uncaught exception in any script, listener or timer goes to the console with its message and to window.onerror; a promise rejected with no handler by the end of the checkpoint goes to the console once as Uncaught (in promise); the page keeps running.
Everything on the engine's own list in Not-written-yet: the v regular-expression flag, lookbehind and property escapes; on the page, import maps, so a bare module specifier fails to resolve. A page whose bundle needs them fails with a syntax error that names the feature, at parse time or at the first call, and the page stands as its markup alone. WebSocket, streams (Response.body is null; a body is read whole), URL.createObjectURL, shadow trees, document.createRange, tree walkers and document.write after the parse are not written either, each throwing or absent rather than pretending.
-
sashfold --render page.html --report out.jsonruns the page's scripts first, their timers on a virtual clock for--script-timemilliseconds (3000 by default);--no-scriptsrenders the markup alone, which is the A of every A/B. The report carries ascriptsobject (run, failed, skipped, external, timers, events, uncaught errors, time), and everyconsole.*line and uncaught error is printed to stderr asconsole.<level>: …. - In a
--scriptreplay,advance <ms> [<times>]moves the pages' clock and runs the timers due, that many times over (so a chain of timers each set by the last runs through), andassert-console <text>checks the page's console output. The window and the replay stop a script that runs past ten seconds. -
tests/test_bindings.cppdrives a realm the way the shell does, under heap stress, andtests/shell/scripting.scriptdrives the shell with a scripted page.
Getting started
What works today
Measured
Design decisions
Testing and tools