Sprint 23: Script-Tags im DOM — parentNode/currentScript für deferred & recaptcha #85

Closed
opened 2026-06-18 20:37:50 +00:00 by Artur · 1 comment
Owner

Problembeschreibung

Aktuell werden ALLE <script> Tags aus dem HTML gestrippt, bevor es an Happy DOM übergeben wird (parser.ts Zeile 82). Dadurch:

  • Scripts können sich nicht via document.querySelector('script[src=...]') selbst finden
  • document.currentScript zeigt auf ein Fake-Element ohne echten parentNode
  • Code der s.parentNode.insertBefore() verwendet (recaptcha, spotify boot) crasht

Betroffene Sites:

  • spotify.com — 19.5KB DOM statt 3MB+ App
  • qwik.dev — deferred Scripts crashen (177KB DOM trotzdem weil main bundle läuft)
  • Jede Site mit recaptcha Bootstrap

Fehlerbild Crawl:

TypeError: undefined is not an object (evaluating 's.parentNode')
  at deferred script: var e = d.querySelector('script[nonce]');
  → s = document.currentScript → fake element ohne parentNode
  → s.parentNode → undefined → TypeError

Aktuelle Architektur (parser.ts)

Parser.start(html):
  1. preloadScripts() — scan ALL script tags aus raw HTML
  2. html.replace(/<script.../gi, "") — STRIP ALLE script tags
  3. document.write(htmlWithoutScripts) — schreibe ins DOM
  4. document.close()
  5. _parserHandledScripts = true  ← ZU SPÄT!
  6. Führe pre-scanned scripts manuell aus

Problem: In Schritt 3-4 feuert Happy DOM's connectedToDocument für jedes <script>. Aber scripts wurden gestrippt → nichts passiert. ABER: die Scripts sind auch nicht im DOM → querySelector('script[src=...]') findet nix.

Option A: Scripts im DOM lassen + early guard (empfohlen)

Parser.start(html):
  1. preloadScripts() — scan ALL script tags
  2. Setze _parserHandledScripts = true ← FRÜHER!
  3. Markiere pre-scanned URLs in DynamicScriptHandler
  4. document.write(html) — VOLLSTÄNDIGES HTML inkl. <script>-Tags
  5. document.close()
  6. disableAllExistingScripts() — setzt disableEvaluation=true
  7. Führe scripts manuell aus (wie bisher)

Erklärung: In Schritt 4 feuert connectedToDocument für jedes <script>. Der DynamicScriptHandler checkt:

  • _parserHandledScripts === true → skippe inline scripts
  • loadedUrls.has(url) → skippe pre-scanned externe scripts
  • Ergebnis: Scripts sind im DOM, aber werden NICHT doppelt ausgeführt

Vorteile:

  • document.querySelector('script[src=...]') findet alle Scripts
  • document.currentScript.parentNode existiert im DOM
  • Kein Refactoring des Parser-Execution-Flows
  • recaptcha Bootstrap funktioniert

Risiken:

  • Happy DOM könnte Scripts doch evaluieren (disableAllExistingScripts muss vor connectedToDocument feuern)
  • Doppelausführung wenn _parserHandledScripts nicht rechtzeitig gesetzt

Option B: Placeholder-Elemente

Ersetze <script> mit <span data-thb-script="true" style="display:none"></span>.
Scripts können Platzhalter finden, aber Happy DOM evaluiert sie nicht.

AbgelehntquerySelector('script[src=...]') findet placeholder nicht (falscher nodeName).

Option C: MutationObserver-block && dann DOM-Injection

Während des HTML-Parsens das MutationObserver-Processing pausieren, dann scripts laden und nachträglich ins DOM injecten.

Abgelehnt — zu komplex, Browserspezifikation sagt Scripts sind während loading im DOM.

Akzeptanzkriterien

  • document.querySelector('script[src=...]') findet MAIN scripts
  • s.parentNode.insertBefore(x, s) crasht nicht (recaptcha)
  • Spotifys initiales Boot-Script läuft ohne TypeError
  • Keine Doppelausführung von pre-scanned Scripts
  • Alle 19 Corpus-Sites passen weiterhin
  • 1519+ Unit Tests pass (zero regression)

Betroffene Dateien

Datei Änderung
src/dom/parser.ts scripts nicht strippen, _parserHandledScripts früher setzen
src/js/dynamic-scripts.ts connectedToDocument: check _parserHandledScripts VOR exec
src/js/execution-realm.ts currentScript auch für fake-Element ohne parentNode
tests/... Neuer Test: querySelector findet script im DOM

Testplan

  1. Unit: parser.ts — kein script-stripping im DOM
  2. Unit: document.currentScript.parentNode existiert während execution
  3. Integration: recaptcha Bootstrap läuft ohne TypeError
  4. Integration: spotify.com DOM > 50KB
  5. E2E: Full corpus 19/19 + detailed error check
## Problembeschreibung Aktuell werden ALLE `<script>` Tags aus dem HTML gestrippt, bevor es an Happy DOM übergeben wird (parser.ts Zeile 82). Dadurch: - Scripts können sich nicht via `document.querySelector('script[src=...]')` selbst finden - `document.currentScript` zeigt auf ein Fake-Element ohne echten parentNode - Code der `s.parentNode.insertBefore()` verwendet (recaptcha, spotify boot) crasht **Betroffene Sites:** - `spotify.com` — 19.5KB DOM statt 3MB+ App - `qwik.dev` — deferred Scripts crashen (177KB DOM trotzdem weil main bundle läuft) - Jede Site mit recaptcha Bootstrap **Fehlerbild Crawl:** ``` TypeError: undefined is not an object (evaluating 's.parentNode') at deferred script: var e = d.querySelector('script[nonce]'); → s = document.currentScript → fake element ohne parentNode → s.parentNode → undefined → TypeError ``` ## Aktuelle Architektur (parser.ts) ``` Parser.start(html): 1. preloadScripts() — scan ALL script tags aus raw HTML 2. html.replace(/<script.../gi, "") — STRIP ALLE script tags 3. document.write(htmlWithoutScripts) — schreibe ins DOM 4. document.close() 5. _parserHandledScripts = true ← ZU SPÄT! 6. Führe pre-scanned scripts manuell aus ``` **Problem:** In Schritt 3-4 feuert Happy DOM's `connectedToDocument` für jedes `<script>`. Aber scripts wurden gestrippt → nichts passiert. ABER: die Scripts sind auch nicht im DOM → `querySelector('script[src=...]')` findet nix. ## Option A: Scripts im DOM lassen + early guard (empfohlen) ``` Parser.start(html): 1. preloadScripts() — scan ALL script tags 2. Setze _parserHandledScripts = true ← FRÜHER! 3. Markiere pre-scanned URLs in DynamicScriptHandler 4. document.write(html) — VOLLSTÄNDIGES HTML inkl. <script>-Tags 5. document.close() 6. disableAllExistingScripts() — setzt disableEvaluation=true 7. Führe scripts manuell aus (wie bisher) ``` **Erklärung:** In Schritt 4 feuert `connectedToDocument` für jedes `<script>`. Der DynamicScriptHandler checkt: - `_parserHandledScripts === true` → skippe inline scripts - `loadedUrls.has(url)` → skippe pre-scanned externe scripts - Ergebnis: Scripts sind im DOM, aber werden NICHT doppelt ausgeführt **Vorteile:** - `document.querySelector('script[src=...]')` findet alle Scripts - `document.currentScript.parentNode` existiert im DOM - Kein Refactoring des Parser-Execution-Flows - recaptcha Bootstrap funktioniert **Risiken:** - Happy DOM könnte Scripts doch evaluieren (disableAllExistingScripts muss vor connectedToDocument feuern) - Doppelausführung wenn _parserHandledScripts nicht rechtzeitig gesetzt ## Option B: Placeholder-Elemente Ersetze `<script>` mit `<span data-thb-script="true" style="display:none"></span>`. Scripts können Platzhalter finden, aber Happy DOM evaluiert sie nicht. **Abgelehnt** — `querySelector('script[src=...]')` findet placeholder nicht (falscher nodeName). ## Option C: MutationObserver-block && dann DOM-Injection Während des HTML-Parsens das MutationObserver-Processing pausieren, dann scripts laden und nachträglich ins DOM injecten. **Abgelehnt** — zu komplex, Browserspezifikation sagt Scripts sind während loading im DOM. ## Akzeptanzkriterien - [ ] `document.querySelector('script[src=...]')` findet MAIN scripts - [ ] `s.parentNode.insertBefore(x, s)` crasht nicht (recaptcha) - [ ] Spotifys initiales Boot-Script läuft ohne TypeError - [ ] Keine Doppelausführung von pre-scanned Scripts - [ ] Alle 19 Corpus-Sites passen weiterhin - [ ] 1519+ Unit Tests pass (zero regression) ## Betroffene Dateien | Datei | Änderung | |-------|----------| | `src/dom/parser.ts` | scripts nicht strippen, _parserHandledScripts früher setzen | | `src/js/dynamic-scripts.ts` | connectedToDocument: check _parserHandledScripts VOR exec | | `src/js/execution-realm.ts` | currentScript auch für fake-Element ohne parentNode | | `tests/...` | Neuer Test: querySelector findet script im DOM | ## Testplan 1. **Unit:** parser.ts — kein script-stripping im DOM 2. **Unit:** `document.currentScript.parentNode` existiert während execution 3. **Integration:** recaptcha Bootstrap läuft ohne TypeError 4. **Integration:** spotify.com DOM > 50KB 5. **E2E:** Full corpus 19/19 + detailed error check
Author
Owner

Issue #85 — Geschlossen (Sprint 19)

Commit: bc0c26b

Was geändert wurde

Vorher Nachher
<script> Tags aus HTML gestrippt vor document.write() Volles HTML inkl. <script> Tags an Happy DOM
querySelector('script[src=...]')null Findet echte Script-Elemente im DOM-Baum
currentScript.parentNodeundefined Echter parentNode aus Happy DOM's DOM-Baum
s.parentNode.insertBefore()TypeError Funktioniert (recaptcha, Spotify)
2 Flags nacheinander gesetzt 3 Flags im Verbund vor document.write()

Neue Architektur

Parser.start(html):
  1. preloadScripts() — scan ALL script tags
  2. _parserHandledScripts = true          ← FRÜHER!
  3. _suppressInlineExecution = true       ← NEU!
  4. document.write(html)                  ← VOLLES HTML inkl. <script>
  5. document.close()
  6. _onDocumentReady() → disableAllExistingScripts()
  7. _suppressInlineExecution = false      ← dynamische Scripts laufen wieder
  8. Execute scripts manually (blocking → defer → module)

Wie connectedToDocument blockt

Script-Typ _parserHandledScripts _suppressInlineExecution Aktion
Inline (initial) true true disableEvaluation=true, skip
Inline (dynamisch) true false Execute via _execInlineScript
External pre-scanned true disableEvaluation=true, skip
External dynamic true Queue für Handler
Ohne Handler (Tests) Fallback: Strip-Verhalten

0 Regression

1519 pass / 4 pre-existing fail

## ✅ Issue #85 — Geschlossen (Sprint 19) **Commit:** `bc0c26b` ### Was geändert wurde | Vorher | Nachher | |--------|---------| | `<script>` Tags aus HTML **gestrippt** vor `document.write()` | **Volles HTML** inkl. `<script>` Tags an Happy DOM | | `querySelector('script[src=...]')` → **null** | ✅ **Findet echte Script-Elemente** im DOM-Baum | | `currentScript.parentNode` → **undefined** | ✅ **Echter parentNode** aus Happy DOM's DOM-Baum | | `s.parentNode.insertBefore()` → `TypeError` | ✅ **Funktioniert** (recaptcha, Spotify) | | 2 Flags nacheinander gesetzt | **3 Flags im Verbund** vor `document.write()` | ### Neue Architektur ``` Parser.start(html): 1. preloadScripts() — scan ALL script tags 2. _parserHandledScripts = true ← FRÜHER! 3. _suppressInlineExecution = true ← NEU! 4. document.write(html) ← VOLLES HTML inkl. <script> 5. document.close() 6. _onDocumentReady() → disableAllExistingScripts() 7. _suppressInlineExecution = false ← dynamische Scripts laufen wieder 8. Execute scripts manually (blocking → defer → module) ``` ### Wie connectedToDocument blockt | Script-Typ | _parserHandledScripts | _suppressInlineExecution | Aktion | |------------|:-----:|:-----:|--------| | Inline (initial) | ✅ true | ✅ true | `disableEvaluation=true`, **skip** | | Inline (dynamisch) | ✅ true | ❌ false | Execute via `_execInlineScript` ✅ | | External pre-scanned | ✅ true | — | `disableEvaluation=true`, **skip** | | External dynamic | ✅ true | — | Queue für Handler ✅ | | Ohne Handler (Tests) | — | — | Fallback: Strip-Verhalten ✅ | ### 0 Regression **1519 pass / 4 pre-existing fail**
Artur closed this issue 2026-06-18 21:07:48 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
glow-all/true-headless-browser#85
No description provided.