Release 1.0.0 - #129
Open
FlorianRappl wants to merge 128 commits into
Open
Conversation
Serialize DocumentReadyState
Upgraded to Jint v3
Updated target frameworks
Added support for JS modules and importmaps
Added Image constructor support
* small cleanup to API usage * add missing license expression * treat warnings as errors to catch obsolete members
Upgrade to Jint 3.1.0
* enable deterministic build
Upgrade to NUKE 8
The release brings the host-object hooks the DOM proxies use in the next commit, and one change that applies without any code here: an object that overrides GetOwnProperty but not Get is now recognised as having ordinary read semantics, so a dotted DOM read probes the node once instead of twice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A node is asked three different things about a property name, and until now all three arrived at GetOwnProperty and got a PropertyDescriptor built for them - including the two that only ever throw it away. Jint 4.15 splits them up, and the node answers each directly: * TryGetOwnPropertyValue hands the value over, so an indexed read no longer allocates a descriptor for the engine to unwrap and drop. Returning false is an authoritative "no own property of that name", which is exactly what the discarded descriptor used to prove. * ProbeOwnProperty answers existence and enumerability, which is what "in", hasOwnProperty, Object.assign, spread and JSON.stringify actually want. An indexed entry no longer has to be converted into a JsValue to be counted. The three share one indexer lookup so that they cannot disagree - the answer one gives has to be the answer the others would give at the same instant, and a Debug build of Jint checks that on every read. The lookup itself now takes the property key rather than its string form, and two things fall out of that: * a key is no longer turned into a String before the type is known to have an indexer at all, and most DOM types have none. A symbol used to compose "Symbol(...)" on every probe, and library code probes Symbol.toStringTag constantly - 83.7 to 2.9 bytes per read; * the string indexer's HasProperty check is handed the key it was given instead of building a JsString for it. That is on the path of every read of an ordinary member of an indexed collection - the length a loop tests - and it is 64 bytes a read, which 4.15 exposes because a host prototype is no longer eligible for the engine's prototype cache. Behaviour is unchanged. The eight tests added to DomTests pass on devel with 4.14.0 as well; they pin the agreement between the three answers, which is the thing this commit could get wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A node list, an HTML collection, a token list and a named node map are all the
same thing to script - a live, read-only, indexed view - and until now every read
of one went through the general node proxy: parse the key, reflect the numeric
indexer off the prototype, invoke it, wrap the result in a descriptor. An index
past the end was signalled by a CLR exception, so a bounds test cost about a
kilobyte.
Jint 4.15 has a base class for exactly this shape. A collection supplies the
members WebIDL says such a thing has - Length, TryGetIndex, and a containment
test - and the engine derives the whole property model from them. Nothing is
cached, so the view stays live.
Containment is the length comparison and nothing else, so every question that
only wants a yes or a no - "in", hasOwnProperty, the key enumerations, the hole
test the Array.prototype generics run per element - stops projecting an element,
wrapping it in a JS value and looking it up in the identity cache just to drop it
again.
Length and the indexed getter are the one place in this binding where reflection
was too slow to leave alone: with the descriptor gone, a MethodInfo.Invoke and its
object[] would have been all that was left of an indexed read. They are compiled
once per type instead. What carries the DOM attributes and what can be called are
not always the same member - IHtmlCollection<T> re-implements
"T IReadOnlyList<T>.this[Int32]" explicitly, and that one is private and abstract
- so the attributed declaration identifies the collection while the public method
of the same name on the concrete class is what runs.
Deciding which proxy to build is a property of the type - an indexed getter plus
a length, the same pair SetIndexer looks for - so it is resolved once per type
for the whole process and never repeats.
There was one proxy class for every DOM object and there are now two, which
cannot share a base: they agree on IDomProxy instead, which is what every caller
outside the proxies actually wanted. Named entries - document.forms.login,
el.attributes.title - are not part of the array-like model and stay on the string
indexer, with indices and length delegated to the base as that base requires. A
named entry whose indexer has no setter is described by a plain value now rather
than by an accessor pair: with nothing to forward a write to, the pair only cost
two function objects per read.
Measured, allocation per operation, net8.0, loop baseline subtracted:
| operation | devel + 4.14.0 | this branch |
| -------------------------------- | -------------- | ----------- |
| list[0] | 326.6 B | 132.1 B |
| list[999] out of range | 1086.1 B | 0.5 B |
| attrs['id'] | 1021.6 B | 278.4 B |
| list.length | 51.9 B | 0.0 B |
| 0 in list | 326.6 B | 0.0 B |
| list.hasOwnProperty(0) | 472.0 B | 0.5 B |
| Array.prototype.forEach.call(..) | 1724.5 B | 568.2 B |
What script sees changes in four ways, all toward what a browser does: for...of,
spread, Array.from and destructuring work on a collection; Object.keys and
for...in report the indices; delete c[0] and defineProperty on an index are
refused; and Array.isArray stays false with the prototype chain untouched, so
instanceof and Symbol.toStringTag are exactly as before.
One deviation is worth stating plainly: "length" becomes an own property of the
collection rather than an inherited one, so hasOwnProperty('length') answers true
where a browser answers false. It is the model the base class defines, it is what
an Array reports too, and reads, "in", enumerability and the value are unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Read the DOM through the host object surface Jint 4.15 adds
… the package needs Two follow-ups to #130. The collection proxy's GetOwnProperty asked the string indexer about a name the array-like base had already answered: an index past the end is an authoritative miss - the value and existence hooks commit to it, the engine trusts them and does not ask again - so an element whose id happens to be numeric surfaced as a descriptor for a key that reads as undefined, answers false to "in" and hasOwnProperty, and never enumerates. WebIDL resolves the same collision the same way: an object supporting indexed properties never serves an array-index name from its named getter. The named lookup now steps aside for canonical array-index keys, classified exactly as the engine classifies them. The package manifest still declared Jint [4.0.0,5.0.0), but the assembly now derives from a base class and overrides hooks that exist only in 4.15.0, and NuGet resolves the lowest version a range admits - a consumer taking the package defaults got an engine the types in it cannot load against. The floor is now the version the code is actually written to. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uV6H9cTntzsoKiaJRBn4f
Keep array-index keys out of the named getter, and declare the Jint the package needs
A DOM prototype was an ObjectInstance subclass of ours, and the engine
refuses such an object as a prototype-method inline-cache holder by
design: a host subclass keeps its own-property set outside the engine,
so no engine-side version can witness that a name is still owned by it.
Every warm member read on a node therefore re-walked to the prototype
and re-resolved the member, which is the one cost three rounds of
optimization never moved.
Prototypes are now instantiated from a JsObjectShape: an immutable
member layout keyed by (type, library set) and held for the whole
process, over which each engine gets an object of its own. The layout is
engine storage, so the carve-out admits it and the cache serves the warm
read. Reflecting over a type tree also happens once per process rather
than once per engine, and existence questions are answered off the
layout without materialising the member they are about.
Everything a prototype carried that belongs to one engine moved onto the
object as host state - the engine, the constructor - and everything that
belongs to the type moved into the shared description: the indexers, and
the event members, whose identity is now the key a node files its
handler under. Member implementations no longer close over an engine and
derive it from the receiver instead. Each proxy remembers what its
prototype knows and checks it against the prototype in force, so the
hottest path costs a field load rather than two type tests.
The window is the exception. Its members are copied onto the global
object and a bare "alert('hi')" calls with no receiver at all, so its
methods keep a function per engine. Its accessors stay shared: an
accessor read always has a receiver, the global object itself for a bare
"document", and that reaches the shaped window prototype.
The registration quirk is reproduced deliberately: a member named after
one of Object.prototype's is skipped, because that is what probing a
half-built prototype used to do. A test pins that nothing in AngleSharp
currently collides, so the choice is made rather than made by accident.
The proxies' own-property hooks are untouched.
Measured on an idle machine against Jint 4.15.2, per read with the loop
baseline subtracted: el.tagName 150.7ns -> 104.7ns, el.hasChildNodes()
158.7ns -> 111.7ns, a script-assigned property 124.0ns -> 101.9ns, an
accessor write at parity, allocation byte-identical on every row. One
whole scripted document costs 1.291ms/578.68KB -> 244.8us/213.54KB.
The Jint floor moves to 4.15.2 for a reason that is not an API one:
JsObjectShape is complete in 4.15.0, but a name absent from a shaped
prototype chain was slower to answer than one absent from a dictionary
chain until jint#2858, and WebIDL makes this binding ask exactly that on
every named lookup a collection serves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every exported type is published as a property of the window, and every one of those properties is copied onto the global object, so `HTMLDivElement` and `Image` and the other names a script may read bare are all backed by the same descriptor. It was a hand-written one carrying `CustomJsValue`, because the constructor object behind a name is only built once script reads it - a document names a handful of the types an assembly exposes. That flag is permanent on such a descriptor, and Jint's global-identifier cache declines a descriptor that could still compute a different value on the next read. It has no way to learn that a custom value became a constant, so the name stayed uncached for the rest of the process even after the one read that fixed its value forever. `PropertyDescriptor.CreateLazy` (Jint 4.15.3) is the same deferral written so the engine can see it end: the descriptor drops the flag the moment it holds a value and rejoins the caches that decline it. The attributes are unchanged - enumerable, neither writable nor configurable - and so is everything a script can observe, which `DeferredConstructorTests` covers in seventeen tests. The floor moves to 4.15.3 for that: it is an API the shipped assembly now uses, which is a stronger reason than the measured one that moved it to 4.15.2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two things this binding depends on were, until Jint 4.15.3, only observable through a diagnostic or not at all. `Engine.Advanced.HasSharedShape` is a supported predicate for "did the shaping actually happen", where `GetObjectRepresentation` is explicitly not part of Jint's compatibility contract. Instantiating a prototype from a member layout falls back to the ordinary per-object dictionary silently and correctly when it cannot shape an object, so nothing about a document would look wrong if a DOM prototype stopped being shaped - only every warm member read on every node would get slower again, which is the entire point of the previous commit. The three prototype kinds now assert it, and the two proxies assert the opposite: a host object of ours shares its layout with nothing, which is the refusal the prototypes had to be moved out of. The host-contract verifiers are the checks that catch a host object answering one of the engine's extension points in a way that contradicts another. This binding answers four of them - `TryGetOwnPropertyValue`, `ProbeOwnProperty`, `HasIndex`, and the semantics derived from not overriding `Get` - and the engine trusts every one without re-checking, so a wrong answer is silent: a member disappears from every enumeration, or a read that should have found an attribute resolves on the prototype. They used to be compiled out of Release, so reaching them meant building Jint from source in Debug, which is a thing a suite does once and then stops doing. Since 4.15.3 the shipped package reads an AppContext switch, so this run - against the very package the library ships against - is a verified one. The switch has to be set before the first use of any Jint type, because the gate behind it is read once at type initialization. A module initializer is the only hook early enough by construction, and it needs a declared attribute on net462 and net472, whose BCL carries none. `TheVerifiersAreRunningForThisSuite` proves the ordering from behaviour rather than from Jint's internal gate: a host whose probe contradicts its own descriptors throws exactly when something is verifying. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Build DOM prototypes from a member layout the process shares
Jint has implemented ES2015 through ES2025 for several releases now, so describing it as "fully ECMAScript 5 compatible" understates what scripts this binding can actually run. The features list gains the module, import map and Web Worker support that has since landed, and names the library fixtures the test suite exercises. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces the hand-maintained nuspec with SDK packaging, so the dependency groups follow the real TargetFrameworks. The nuspec had drifted: it declared net6.0 and net7.0 groups that are not built, and no group for net10.0, which is shipped. Drops Microsoft.SourceLink.GitHub — SourceLink ships in the .NET SDK since 8.0.100 and the repository metadata (branch, commit) now lands in the package without it. Centralizes package versions in src/Directory.Packages.props. AngleSharp and Jint are stated as ranges because `dotnet pack` publishes them verbatim; the 2.0 / 5.0 ceilings the nuspec carried are preserved, and the ceiling now holds even when CI overrides the floor via -AngleSharpVersion. Points the test project at net10.0 and drops the netstandard2.0 pin on its ProjectReference, so tests exercise the library build they ship against rather than always the netstandard2.0 one. Also converts the solution to .slnx (which drops four orphaned project-config blocks), moves both AssemblyInfo.cs files into the csproj, adds a global.json, and upgrades NUKE to 10.1.0. The produced package is byte-comparable to the one devel produces, apart from the net10.0 group being added, the dead net6.0/net7.0 groups being removed, and SourceLink metadata being present. Assembly identity is unchanged: Version=1.0.0.0, PublicKeyToken=e83494dcdc6d31ea. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replace the ES5-era claims in the README
Reverts the .slnx conversion per review: the format change is not worth narrowing the tooling contributors can use, and the benefit was small. Keeps the part that was actually broken — four ProjectConfiguration blocks referencing project GUIDs that no longer exist in the solution. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Removes the global.json added earlier in this branch, per review: the absence is intentional, not an oversight, and AGENTS.md now says so rather than describing it as a gap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two things the core repo already does better, found while porting this change to AngleSharp/AngleSharp#1272. Drops the AssemblyVersion pin. Published AngleSharp bumps AssemblyVersion with every minor (1.5.0.0, 1.6.0.0, 1.7.0.0), so freezing it here would have made the two packages follow opposite policies. Restores <Version> as the fallback for builds that bypass the NUKE script, matching the core layout. The built assembly is unchanged either way at 1.0.0.0. Replaces the GITHUB_ACTIONS environment gate for ContinuousIntegrationBuild with .SetContinuousIntegrationBuild(IsServerBuild) on Compile and pack, which is what nuke/Build.cs in the core repo does. NUKE's host detection is not tied to one CI provider, and the core repo's published symbols prove the mechanism works: AngleSharp 1.6.0 has zero absolute paths. Verified: package metadata and dependency ranges are byte-identical to the previous state of this branch apart from the SourceLink commit hash. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Suggestion: modernize the project files and generate the package with dotnet pack
NUKE is unmaintained and its transitive dependency graph keeps producing advisories. Fallout is the maintained hard fork, so the move is mostly a namespace swap plus the change from a bootstrapped build project to Fallout's tool-manifest model. `fallout-migrate` handled the mechanical part: `Nuke.*` -> `Fallout.*`, `NukeBuild` -> `FalloutBuild`, the `<Nuke*>` MSBuild properties, and `.nuke/` -> `.fallout/`. The rest needed hand work: - `Nuke.Common.ProjectModel` is not a 1:1 map — `Solution` and `Project` now live in `Fallout.Solutions`. - `build.ps1` / `build.sh` become thin shims. They still provision an SDK, but instead of building and running `_build.csproj` they end with `dotnet tool restore` + `dotnet fallout`, so the orchestrator version is pinned in `.config/dotnet-tools.json` (`fallout.cli`, not the unpublished `Fallout.GlobalTools` the docs name). The `NUKE_ENTERPRISE_TOKEN` feedz block is gone. - `nuke/` -> `build/`, which is where Fallout's CLI resolves `_build.csproj` by convention when `.fallout/parameters.json` does not name one. - Dropped the `NuGet.Packaging` CVE pin — Fallout 11.0.18 resolves a clean 6.14.3. The `System.Security.Cryptography.Xml` pin stays: 11.0.18 still ships 10.0.6, and the fix upstream is committed but unreleased. `build.ps1`, `build.sh` and `.gitignore` are normally synced from AngleSharp.GitBase; the migration cannot avoid touching them, so those edits need folding back upstream. Verified with `.\build.ps1 RunUnitTests -AngleSharpVersion 1.5.0`: 286 tests pass on net10.0, net462 and net472, and `-Target` plus parameter passing behave as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All three actions were a major behind: `actions/checkout` v5 → v7, `actions/setup-dotnet` v5 → v6, `actions/setup-node` v5 → v7. Each of those majors is an ESM migration plus dependency upgrades, with no input changes affecting this workflow. checkout v7 additionally blocks checking out fork PRs under `pull_request_target`/`workflow_run`, which this workflow does not use — it triggers on `push` and `pull_request`. Runtime versions are deliberately left alone. `dotnet-version: 10.0.x` is already the current LTS, supported to November 2028. `node-version` stays at 20.x: it only feeds the doclet publish, whose npm dependencies are old enough that moving the runtime risks breaking them for no benefit here. Also replaced the `::set-output` workflow command, deprecated since 2022 and no longer functional, with an append to `$GITHUB_OUTPUT`. It is what feeds `needs.can_document.outputs.value`, so the documentation job's condition could not evaluate true while it stayed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Migrate the build orchestrator from NUKE to Fallout, and refresh the CI workflow actions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Types of Changes
Prerequisites
Please make sure you can check the following two boxes:
Contribution Type
What types of changes does your code introduce? Put an
xin all the boxes that apply:Description
Note that I mentioned "Breaking change" even though I don't think anything will break when updating to this release. In general people will be amazed how much is now working... at least that's my hope.
Shoutout to everyone who made this possible. Years ago I thought this project is a dead-end and finished - now I think this might be the foundation for something really great. Without people like @lahma, @arekdygas, and @tomvanenckevort the project could not have been brought to this level. Much appreciated everyone!
(PR is there to be merged once final tests and cosmetics are in place - if you see anything or want to improve it just open a PR to
devel)