Script Loader: classic/defer/async/module orchestration + module graph #12

Closed
opened 2026-06-17 13:37:44 +00:00 by Artur · 1 comment
Owner

Goal

Implement the Script Loader — the core of the project. Orchestrate script loading and execution order per HTML spec: classic (blocking), defer, async, module, dynamic import.

What to Build

src/js/script-loader.ts

export type ScriptType = 'classic-blocking' | 'classic-defer' | 'classic-async' | 'module' | 'inline' | 'dynamic-import';

export interface ScriptEntry {
  type: ScriptType;
  url: string | null;         // null for inline scripts
  content: string | null;     // null for external scripts (fetched)
  element: HTMLScriptElement | null; // null for dynamic imports
  order: number;              // document order index
  moduleGraph?: string[];     // dependency URLs
}

export class ScriptLoader {
  constructor();

  // Called by parser when encountering a <script> tag
  onScriptTag(script: HTMLScriptElement): Promise<void>;

  // Fetch and execute (handles all script types)
  async executeScript(entry: ScriptEntry): Promise<void>;

  // Execute all deferred scripts (called after parser finishes)
  async executeDeferredScripts(): Promise<void>;

  // Execute all async scripts as they finish loading
  async executeAsyncScript(entry: ScriptEntry): Promise<void>;

  // Module graph resolution + execution
  async executeModuleScript(entry: ScriptEntry): Promise<void>;

  // Check if blocking scripts are pending
  hasPendingBlocking(): boolean;

  // Check if modules are being resolved
  hasPendingModules(): boolean;

  // DocumentContentLoaded fire
  async fireDOMContentLoaded(): Promise<void>;

  // Load event
  async fireLoadEvent(): Promise<void>;
}

Execution Order (HTML Spec)

1. Parser encounters <script src="a.js"> (no attrs)
   -> PARSER PAUSES
   -> Fetch a.js
   -> Execute a.js immediately
   -> PARSER RESUMES

2. Parser encounters <script defer src="b.js">
   -> Start fetching b.js (parallel)
   -> Parser continues
   -> After parser finishes: execute defer scripts in document order
   -> Fire DOMContentLoaded

3. Parser encounters <script async src="c.js">
   -> Start fetching c.js (parallel)
   -> Parser continues
   -> When c.js finishes loading: execute immediately (any order)
   -> Parser remains unpaused

4. Parser encounters <script type="module" src="d.js">
   -> Start fetching d.js + its dependencies (parallel)
   -> Parser continues
   -> Module graph resolution determines execution order
   -> Execute in document order (like defer)

5. Parser encounters <script nomodule src="e.js">
   -> IGNORED (we support modules)

6. Parser encounters <script> inline code </script>
   -> Execute immediately (blocking)
   -> PARSER PAUSES during execution
   -> PARSER RESUMES after execution

7. Dynamic import('module.js')
   -> Executed when the import() statement runs (during page JS)
   -> Fetch + execute on demand

Module Graph Resolution

interface ModuleGraph {
  // Map<url, { imports: string[], importedBy: string[], executed: boolean }>
  
  add(scriptUrl: string, importSpecifiers: string[]): void;
  
  // Resolve dependency graph and produce execution order
  resolve(): Promise<string[]>;
  
  // Check for circular dependencies
  hasCircular(): boolean;
}

Script Fetcher

async function fetchScript(url: string, baseUrl: string): Promise<string> {
  // Use Bun.fetch to download script content
  // Support: HTTP, HTTPS, relative URLs (resolved against base)
  // Cache: loaded scripts are cached per context
  // Errors: log + throw or return empty string?
}

Tests

Unit Tests (THE MOST IMPORTANT SUITE)

Test Verifies
script-loader.classic-blocking.test.ts Classic script pauses parser, executes immediately
script-loader.defer-order.test.ts Defer scripts execute in document order after parse
script-loader.async-order.test.ts Async scripts execute when fetched (any order)
script-loader.module-order.test.ts Module scripts execute after deps resolved, doc order
script-loader.inline.test.ts Inline script executes immediately
script-loader.nomodule.test.ts nomodule scripts are skipped
script-loader.dom-content-loaded.test.ts DOMContentLoaded fires after defer scripts
script-loader.load-event.test.ts Load event fires after all scripts
script-loader.mixed.test.ts Mix of classic + defer + async + module executes correctly
script-loader.dynamic-import.test.ts import() executes on demand
module-graph.basic.test.ts Simple dependency graph resolves correctly
module-graph.chain.test.ts A imports B imports C resolves in correct order
module-graph.circular.test.ts Circular dependencies detected

Integration Tests

Test Verifies
script-loader.react-fixture.test.ts React app renders via loaded scripts
script-loader.vue-fixture.test.ts Vue app renders
script-loader.google-maps-fixture.test.ts Maps JS loads without crashing (no maps DOM req'd)

Definition of Done

  • src/js/script-loader.ts with ScriptLoader class
  • src/js/module-graph.ts with ModuleGraph class
  • Classic (blocking) script execution
  • Defer script execution
  • Async script execution
  • Module script execution + graph resolution
  • Dynamic import() support
  • DOMContentLoaded + Load event firing
  • All unit tests pass (especially mixed-order tests)
  • 100% line + branch coverage
## Goal Implement the Script Loader — the **core** of the project. Orchestrate script loading and execution order per HTML spec: classic (blocking), defer, async, module, dynamic import. ## What to Build ### src/js/script-loader.ts ``` export type ScriptType = 'classic-blocking' | 'classic-defer' | 'classic-async' | 'module' | 'inline' | 'dynamic-import'; export interface ScriptEntry { type: ScriptType; url: string | null; // null for inline scripts content: string | null; // null for external scripts (fetched) element: HTMLScriptElement | null; // null for dynamic imports order: number; // document order index moduleGraph?: string[]; // dependency URLs } export class ScriptLoader { constructor(); // Called by parser when encountering a <script> tag onScriptTag(script: HTMLScriptElement): Promise<void>; // Fetch and execute (handles all script types) async executeScript(entry: ScriptEntry): Promise<void>; // Execute all deferred scripts (called after parser finishes) async executeDeferredScripts(): Promise<void>; // Execute all async scripts as they finish loading async executeAsyncScript(entry: ScriptEntry): Promise<void>; // Module graph resolution + execution async executeModuleScript(entry: ScriptEntry): Promise<void>; // Check if blocking scripts are pending hasPendingBlocking(): boolean; // Check if modules are being resolved hasPendingModules(): boolean; // DocumentContentLoaded fire async fireDOMContentLoaded(): Promise<void>; // Load event async fireLoadEvent(): Promise<void>; } ``` ### Execution Order (HTML Spec) ``` 1. Parser encounters <script src="a.js"> (no attrs) -> PARSER PAUSES -> Fetch a.js -> Execute a.js immediately -> PARSER RESUMES 2. Parser encounters <script defer src="b.js"> -> Start fetching b.js (parallel) -> Parser continues -> After parser finishes: execute defer scripts in document order -> Fire DOMContentLoaded 3. Parser encounters <script async src="c.js"> -> Start fetching c.js (parallel) -> Parser continues -> When c.js finishes loading: execute immediately (any order) -> Parser remains unpaused 4. Parser encounters <script type="module" src="d.js"> -> Start fetching d.js + its dependencies (parallel) -> Parser continues -> Module graph resolution determines execution order -> Execute in document order (like defer) 5. Parser encounters <script nomodule src="e.js"> -> IGNORED (we support modules) 6. Parser encounters <script> inline code </script> -> Execute immediately (blocking) -> PARSER PAUSES during execution -> PARSER RESUMES after execution 7. Dynamic import('module.js') -> Executed when the import() statement runs (during page JS) -> Fetch + execute on demand ``` ### Module Graph Resolution ``` interface ModuleGraph { // Map<url, { imports: string[], importedBy: string[], executed: boolean }> add(scriptUrl: string, importSpecifiers: string[]): void; // Resolve dependency graph and produce execution order resolve(): Promise<string[]>; // Check for circular dependencies hasCircular(): boolean; } ``` ### Script Fetcher ``` async function fetchScript(url: string, baseUrl: string): Promise<string> { // Use Bun.fetch to download script content // Support: HTTP, HTTPS, relative URLs (resolved against base) // Cache: loaded scripts are cached per context // Errors: log + throw or return empty string? } ``` ## Tests ### Unit Tests (THE MOST IMPORTANT SUITE) | Test | Verifies | |------|----------| | script-loader.classic-blocking.test.ts | Classic script pauses parser, executes immediately | | script-loader.defer-order.test.ts | Defer scripts execute in document order after parse | | script-loader.async-order.test.ts | Async scripts execute when fetched (any order) | | script-loader.module-order.test.ts | Module scripts execute after deps resolved, doc order | | script-loader.inline.test.ts | Inline script executes immediately | | script-loader.nomodule.test.ts | nomodule scripts are skipped | | script-loader.dom-content-loaded.test.ts | DOMContentLoaded fires after defer scripts | | script-loader.load-event.test.ts | Load event fires after all scripts | | script-loader.mixed.test.ts | Mix of classic + defer + async + module executes correctly | | script-loader.dynamic-import.test.ts | import() executes on demand | | module-graph.basic.test.ts | Simple dependency graph resolves correctly | | module-graph.chain.test.ts | A imports B imports C resolves in correct order | | module-graph.circular.test.ts | Circular dependencies detected | ### Integration Tests | Test | Verifies | |------|----------| | script-loader.react-fixture.test.ts | React app renders via loaded scripts | | script-loader.vue-fixture.test.ts | Vue app renders | | script-loader.google-maps-fixture.test.ts | Maps JS loads without crashing (no maps DOM req'd) | ## Definition of Done - [ ] src/js/script-loader.ts with ScriptLoader class - [ ] src/js/module-graph.ts with ModuleGraph class - [ ] Classic (blocking) script execution - [ ] Defer script execution - [ ] Async script execution - [ ] Module script execution + graph resolution - [ ] Dynamic import() support - [ ] DOMContentLoaded + Load event firing - [ ] All unit tests pass (especially mixed-order tests) - [ ] 100% line + branch coverage
Author
Owner

Script Loader: classic/defer/async/module orchestration + module graph. Implementiert in src/js/script-loader.ts. Tests: script-loader.test.ts, script-pipeline.test.ts.

Script Loader: classic/defer/async/module orchestration + module graph. ✅ Implementiert in src/js/script-loader.ts. Tests: script-loader.test.ts, script-pipeline.test.ts.
Artur closed this issue 2026-06-18 06:28:03 +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#12
No description provided.