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