From 8af1e07c0cf60fd86727c661d9123daa7905d96a Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Mon, 27 Jul 2026 10:36:27 +0300 Subject: [PATCH] Add AI agent guidance in AGENTS.md Documents the build and test commands, the reflection-based binding architecture, the performance expectations, the AngleSharp code conventions and the test idioms, so an agent does not have to rediscover them from the sources every session. CLAUDE.md only imports it, keeping one file for every agent. Co-Authored-By: Claude Opus 5 (1M context) --- AGENTS.md | 212 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 8 +++ 2 files changed, 220 insertions(+) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f78f1f2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,212 @@ +# AGENTS.md + +Guidance for AI coding agents working in this repository. `CLAUDE.md` imports this file, so +keep it the single source of truth and every agent reads the same instructions. + +AngleSharp.Js is an AngleSharp plugin that exposes the AngleSharp DOM to the +[Jint](https://github.com/sebastienros/jint) JavaScript engine. There is no code generation +and no hand-written binding layer: the whole JS surface is derived at runtime by reflecting +over AngleSharp's `[DomName]`-style attributes. + +## Commands + +The orchestrator is NUKE (`nuke/Build.cs`), bootstrapped by `build.ps1` / `build.sh` +(`build.cmd` forwards to either). Default target is `RunUnitTests`. + +```powershell +.\build.ps1 # restore, compile, run the full test suite +.\build.ps1 -Target Compile # other targets: Clean Restore Compile RunUnitTests +.\build.ps1 -Target Package # CopyFiles CreatePackage Package PrePublish Publish +``` + +For the normal edit/test loop use the SDK directly — much faster than the NUKE bootstrap: + +```powershell +dotnet build src/AngleSharp.Js.sln +dotnet test src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj -f net8.0 +dotnet test src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj -f net8.0 --filter "FullyQualifiedName~InstanceOfTests" +dotnet test src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj -f net8.0 --filter "Name=WindowIsAnInstanceOfWindow" +``` + +- Always pass `-f net8.0` when iterating. On Windows both projects also target `net462` and + `net472`, so omitting it runs everything three times. +- `TreatWarningsAsErrors` is on (`src/Directory.Build.props`) — a warning breaks the build. + There is no separate lint step; the compiler is it. +- The package version is parsed from the top entry of `CHANGELOG.md` (`ReleaseNotesParser`), + not from a csproj property. Release-worthy changes get a `CHANGELOG.md` line. +- `RunUnitTests` runs the suite twice, differing only in a `prefetched` environment variable + that nothing in this repo currently reads — a single run is equivalent locally. +- There is no `global.json`; the bootstrap scripts use the STS channel, CI installs 10.0.x. + +## Architecture + +### Wiring into AngleSharp + +`WithJs()` (`JsConfigurationExtensions`) registers four things: `JsScriptingService`, an +`EventAttributeObserver` (inline `onclick="…"` attributes), a `JsNavigationHandler` +(`javascript:` URLs), and a default `INavigator`. `WithEventLoop()` adds `JsEventLoop`, a +dedicated background thread that serializes script tasks — most DOM-mutating tests need it. + +`JsScriptingService` is AngleSharp's `IScriptingService`. It keeps one `EngineInstance` per +`IWindow` in a `ConditionalWeakTable`, and accepts `text/javascript`, `module` and +`importmap`. **The set of assemblies whose types get exposed is derived from the services +registered in the browsing context** (every `AngleSharp*` assembly behind a registered +service), so adding `WithCss()` or `WithIo()` widens the JS DOM surface. + +`EngineInstance` owns the Jint `Engine` and the per-engine caches. On construction it wraps +the window, walks each library's exported types to publish constructors, constructor +functions and instances, then copies the window's own properties onto the Jint global and +points the global's prototype at the window prototype. `RunScript` locks on the engine. + +### The proxy objects (`src/AngleSharp.Js/Proxies`) + +- **`DomNodeInstance`** — the JS object standing for one CLR DOM object. Identity is + preserved by `ReferenceCache` (a `ConditionalWeakTable`), so the same node always maps to + the same JS object. Only indexers become own properties; interface members live on the + prototype. Writes to the window instance are mirrored onto the Jint global. +- **`DomPrototypeInstance`** — one per DOM type. Members are registered lazily in + `Initialize()`: reflecting the full type tree is the expensive part and most prototypes + are never touched. Also owns the numeric/string indexers and extension members pulled from + `[DomExposed]` types. +- **`DomConstructorInstance`** — the exposed constructor (`HTMLDivElement`, …). The + prototype holds it, so the object script reads off the window and the one an instance + reports as its `constructor` are the same. It answers `Symbol.hasInstance` itself, because + the prototype chain cannot cover mixins (`ParentNode`) or generic collections + (`IHtmlCollection`). +- **`DomConstructorDescriptor`** — the property a constructor is published under. Every + exported type gets one, so the constructor object behind it is built only on first read. + +### Type → prototype canonicalization + +This is the subtlety most changes trip over. Instances are created from internal concrete +classes (`HtmlDivElement`), constructors are built from exported interfaces +(`IHtmlDivElement`), and many element classes carry no `[DomName]` of their own (a `b` +element is just an `HTMLElement`). `PrototypeTypeCache` folds all of these onto the topmost +class that defines a given DOM name, which is what makes `instanceof` and +`Object.getPrototypeOf(div) === HTMLDivElement.prototype` hold. Enums and generic types are +deliberately excluded from folding. + +### Caching rules (`src/AngleSharp.Js/Cache`) + +Split by what the cached value depends on: + +- Process-wide statics — `CreatorCache`, `PrototypeTypeCache`, `MethodCache`, `ScriptCache` + (capped at 32 prepared scripts) — hold values determined by a type, method or source alone. +- Per-engine — `PrototypeCache`, `ReferenceCache` — hold anything that is a `JsValue` or + otherwise bound to one engine, plus anything depending on the engine's library set. + +Anything shared across engines must be thread-safe; the existing caches all use +`Concurrent*` collections. + +### Marshalling + +`EngineExtensions.ToJsValue` and `JsValueExtensions.FromJsValue` / `.As(type, …)` convert +values. `EngineExtensions.BuildArgs` maps JS arguments onto a CLR signature and handles the +DOM-specific cases: an implicit leading `IWindow` parameter, optional parameters, `params` +arrays, and `[DomInitDict]` option objects expanded into positional arguments. + +## Performance + +**Performance is a primary concern in this repository, not an afterthought.** Jint is an +interpreter and every DOM access from script crosses this binding layer, so the binding must +never be the bottleneck. Reflection is the whole mechanism here, and reflection is slow — the +work of the last release cycle was largely making it happen once instead of once per call +(`perf/cache-interop-reflection`, `perf/cache-parsed-scripts`, `perf/lazy-dom-prototypes`, +`perf/lazy-dom-constructors`). Hold new code to that standard. + +The two techniques that carry most of the win: + +- **Cache anything derived from a type, method or source string.** It cannot change for the + lifetime of the process. `MethodDescription.Of` resolves a signature once; + `CreatorCache.GetConstructorDefinition` caches even the *null* answer, because most + exported types are not constructors; `ScriptCache` keeps Jint's `Prepared