@miaskiewicz/turbo-dom 0.1.34 → 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miaskiewicz/turbo-dom",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "description": "Faster, more spec-correct DOM for test runners — native html5ever (Rust/WASM) parser + lazy copy-on-write DOM. A drop-in-style alternative to jsdom/happy-dom for vitest & jest.",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
@@ -1362,7 +1362,7 @@ export class Document extends Node {
1362
1362
  this.__cache = []; // idx -> handle (identity memoization / nodeAt)
1363
1363
  this.__active = null; // activeElement
1364
1364
  this.defaultView = null; // set by environment (window)
1365
- this.__mo = []; // registered MutationObservers
1365
+ this.__mo = null; // registered MutationObservers (lazy — most files never observe())
1366
1366
  this.__moPending = null; // observers with queued records awaiting microtask
1367
1367
  }
1368
1368
  get nodeType() { return DOCUMENT_NODE; }
@@ -1371,10 +1371,11 @@ export class Document extends Node {
1371
1371
  // ---- MutationObserver registry ----
1372
1372
  __moRegister(obs, target, options) {
1373
1373
  // replace existing registration for (obs,target) per spec
1374
- this.__mo = this.__mo.filter((r) => !(r.obs === obs && r.target === target));
1374
+ if (!this.__mo) this.__mo = [];
1375
+ else this.__mo = this.__mo.filter((r) => !(r.obs === obs && r.target === target));
1375
1376
  this.__mo.push({ obs, target, options });
1376
1377
  }
1377
- __moUnregister(obs) { this.__mo = this.__mo.filter((r) => r.obs !== obs); }
1378
+ __moUnregister(obs) { if (this.__mo) this.__mo = this.__mo.filter((r) => r.obs !== obs); }
1378
1379
  __scheduleMO(obs) {
1379
1380
  if (!this.__moPending) this.__moPending = new Set();
1380
1381
  if (this.__moPending.has(obs)) return;
@@ -79,6 +79,41 @@ function tagClass(matcher) {
79
79
  // the bare names on globalThis (which would make these delegates call themselves).
80
80
  const hostSetTimeout = globalThis.setTimeout;
81
81
  const hostClearTimeout = globalThis.clearTimeout;
82
+
83
+ // Env-independent lazy global factories — built ONCE at module load, shared by
84
+ // every window (none capture document/url/windowProxy). Materialization still
85
+ // self-replaces onto the per-env base, so each env gets its own instance; only
86
+ // the factory objects/closures are shared, saving ~28 allocations per test file.
87
+ const SHARED_LAZY = {
88
+ localStorage: () => new Storage(),
89
+ sessionStorage: () => new Storage(),
90
+ matchMedia: () => makeMatchMedia(),
91
+ getComputedStyle: () => makeGetComputedStyle(),
92
+ IntersectionObserver: () => IntersectionObserver,
93
+ ResizeObserver: () => ResizeObserver,
94
+ requestAnimationFrame: () => (cb) => hostSetTimeout(() => cb(performanceNow()), 0),
95
+ cancelAnimationFrame: () => (id) => hostClearTimeout(id),
96
+ navigator: () => ({
97
+ userAgent: 'Mozilla/5.0 (turbo-dom) AppleWebKit/537.36',
98
+ platform: 'turbo-dom', vendor: '', language: 'en-US', languages: ['en-US'],
99
+ onLine: true, cookieEnabled: true, doNotTrack: null, maxTouchPoints: 0,
100
+ hardwareConcurrency: 4, deviceMemory: 8, webdriver: false,
101
+ clipboard: { readText: async () => '', writeText: async () => {}, read: async () => [], write: async () => {} },
102
+ permissions: { query: async () => ({ state: 'prompt', addEventListener() {}, removeEventListener() {} }) },
103
+ sendBeacon: () => true, vibrate: () => false,
104
+ }),
105
+ performance: () => ({ now: performanceNow, timeOrigin: 0, mark() {}, measure() {}, getEntriesByName: () => [], getEntriesByType: () => [], clearMarks() {}, clearMeasures() {} }),
106
+ Storage: () => Storage,
107
+ devicePixelRatio: () => 1,
108
+ innerWidth: () => 1024,
109
+ innerHeight: () => 768,
110
+ outerWidth: () => 1024,
111
+ outerHeight: () => 768,
112
+ scrollX: () => 0, scrollY: () => 0, pageXOffset: () => 0, pageYOffset: () => 0,
113
+ screenX: () => 0, screenY: () => 0, screenLeft: () => 0, screenTop: () => 0,
114
+ screen: () => ({ width: 1024, height: 768, availWidth: 1024, availHeight: 768, colorDepth: 24, pixelDepth: 24, orientation: { type: 'landscape-primary', angle: 0, addEventListener() {}, removeEventListener() {} } }),
115
+ visualViewport: () => ({ width: 1024, height: 768, scale: 1, offsetLeft: 0, offsetTop: 0, pageLeft: 0, pageTop: 0, addEventListener() {}, removeEventListener() {} }),
116
+ };
82
117
  const hostSetInterval = globalThis.setInterval;
83
118
  const hostClearInterval = globalThis.clearInterval;
84
119
  const hostQueueMicrotask = globalThis.queueMicrotask;
@@ -107,39 +142,13 @@ export function createWindow(document, { url = 'http://localhost/' } = {}) {
107
142
  removeEventListener: (...a) => document.removeEventListener(...a),
108
143
  };
109
144
 
110
- // Lazy globals none constructed until touched.
145
+ // Per-env lazy globals: ONLY those that capture document/url/windowProxy.
146
+ // Everything env-independent lives in the module-level SHARED_LAZY (built once,
147
+ // not re-allocated per test file) — see below. None constructed until touched.
111
148
  const lazy = {
112
- localStorage: () => new Storage(),
113
- sessionStorage: () => new Storage(),
114
- matchMedia: () => makeMatchMedia(),
115
- getComputedStyle: () => makeGetComputedStyle(),
116
- IntersectionObserver: () => IntersectionObserver,
117
- ResizeObserver: () => ResizeObserver,
118
- requestAnimationFrame: () => (cb) => hostSetTimeout(() => cb(performanceNow()), 0),
119
- cancelAnimationFrame: () => (id) => hostClearTimeout(id),
120
149
  // subsystem grouping: history co-materializes with (and shares) location
121
150
  location: () => makeLocation(url),
122
151
  history: () => makeHistory(windowProxy.location),
123
- navigator: () => ({
124
- userAgent: 'Mozilla/5.0 (turbo-dom) AppleWebKit/537.36',
125
- platform: 'turbo-dom', vendor: '', language: 'en-US', languages: ['en-US'],
126
- onLine: true, cookieEnabled: true, doNotTrack: null, maxTouchPoints: 0,
127
- hardwareConcurrency: 4, deviceMemory: 8, webdriver: false,
128
- clipboard: { readText: async () => '', writeText: async () => {}, read: async () => [], write: async () => {} },
129
- permissions: { query: async () => ({ state: 'prompt', addEventListener() {}, removeEventListener() {} }) },
130
- sendBeacon: () => true, vibrate: () => false,
131
- }),
132
- performance: () => ({ now: performanceNow, timeOrigin: 0, mark() {}, measure() {}, getEntriesByName: () => [], getEntriesByType: () => [], clearMarks() {}, clearMeasures() {} }),
133
- Storage: () => Storage,
134
- devicePixelRatio: () => 1,
135
- innerWidth: () => 1024,
136
- innerHeight: () => 768,
137
- outerWidth: () => 1024,
138
- outerHeight: () => 768,
139
- scrollX: () => 0, scrollY: () => 0, pageXOffset: () => 0, pageYOffset: () => 0,
140
- screenX: () => 0, screenY: () => 0, screenLeft: () => 0, screenTop: () => 0,
141
- screen: () => ({ width: 1024, height: 768, availWidth: 1024, availHeight: 768, colorDepth: 24, pixelDepth: 24, orientation: { type: 'landscape-primary', angle: 0, addEventListener() {}, removeEventListener() {} } }),
142
- visualViewport: () => ({ width: 1024, height: 768, scale: 1, offsetLeft: 0, offsetTop: 0, pageLeft: 0, pageTop: 0, addEventListener() {}, removeEventListener() {} }),
143
152
  };
144
153
 
145
154
  windowProxy = new Proxy(base, {
@@ -147,7 +156,7 @@ export function createWindow(document, { url = 'http://localhost/' } = {}) {
147
156
  if (k === 'window' || k === 'self' || k === 'globalThis' || k === 'parent' || k === 'top') return windowProxy;
148
157
  if (k in t) return t[k]; // per-env (incl. overrides + materialized lazy)
149
158
  if (k in STATIC_BASE) return STATIC_BASE[k]; // shared stateless globals
150
- const factory = lazy[k];
159
+ const factory = lazy[k] || SHARED_LAZY[k];
151
160
  if (factory) {
152
161
  touched.add(k);
153
162
  const v = factory();
@@ -158,7 +167,7 @@ export function createWindow(document, { url = 'http://localhost/' } = {}) {
158
167
  },
159
168
  set(t, k, v) { t[k] = v; return true; }, // writes to base → shadows STATIC_BASE per-env
160
169
  has(t, k) {
161
- return k in t || k in STATIC_BASE || k in lazy ||
170
+ return k in t || k in STATIC_BASE || k in lazy || k in SHARED_LAZY ||
162
171
  k === 'window' || k === 'self' || k === 'globalThis' || k === 'parent' || k === 'top';
163
172
  },
164
173
  // so vi.spyOn(window, 'scrollTo'/'open'/…) finds STATIC_BASE methods as own
@@ -178,7 +187,7 @@ export function createWindow(document, { url = 'http://localhost/' } = {}) {
178
187
  // which lazy globals this test materialized (the "DOM surface used" report)
179
188
  touched: () => [...touched],
180
189
  // every global name this window can provide (for environment adapters)
181
- globalKeys: [...Object.keys(base), ...Object.keys(STATIC_BASE), ...Object.keys(lazy)],
190
+ globalKeys: [...Object.keys(base), ...Object.keys(STATIC_BASE), ...Object.keys(lazy), ...Object.keys(SHARED_LAZY)],
182
191
  // Layer 5: drop materialized global slots, keep the class machinery warm.
183
192
  resetGlobals() {
184
193
  for (const k of touched) delete base[k];