@miaskiewicz/turbo-dom 0.1.21 → 0.1.22

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.21",
3
+ "version": "0.1.22",
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",
@@ -87,72 +87,20 @@ export function createWindow(document, { url = 'http://localhost/' } = {}) {
87
87
  const touched = new Set();
88
88
  let windowProxy;
89
89
 
90
- // Universal globals (touched by ~every test) eager, no point lazifying.
90
+ // Per-env globals: only the ~11 that capture document/url/windowProxy. Everything
91
+ // stateless (constructors, timers, stateless window methods) lives in the shared
92
+ // module-level STATIC_BASE, built ONCE — not rebuilt per createWindow (per test
93
+ // file). The Proxy below reads base first, then STATIC_BASE, then lazy; a
94
+ // `window.x = y` assignment writes to base, shadowing STATIC_BASE per-env.
91
95
  const base = {
92
96
  document,
93
97
  name: '',
94
98
  closed: false,
95
99
  origin: new URL(url).origin,
96
- // constructors are cheap class refs — expose eagerly
97
- Node, Element, Text, Comment, Document, DocumentFragment, DocumentType,
98
- EventTarget,
99
- Event, CustomEvent,
100
- UIEvent, MouseEvent, PointerEvent, KeyboardEvent, InputEvent, FocusEvent,
101
- CompositionEvent, WheelEvent, TouchEvent, DragEvent, ProgressEvent, ClipboardEvent,
102
- DataTransfer,
103
- // every element is a plain Element → `el instanceof HTMLElement` is true.
104
- // Tag-specific interfaces match by localName via Symbol.hasInstance, so
105
- // `el instanceof HTMLAnchorElement` is true ONLY for <a> (not every element),
106
- // and React's `while (el instanceof HTMLIFrameElement)` loop terminates.
107
- HTMLElement: Element, SVGElement: Element,
108
- HTMLAnchorElement: tagClass('a'), HTMLInputElement: tagClass('input'),
109
- HTMLTextAreaElement: tagClass('textarea'), HTMLSelectElement: tagClass('select'),
110
- HTMLOptionElement: tagClass('option'), HTMLButtonElement: tagClass('button'),
111
- HTMLFormElement: tagClass('form'), HTMLImageElement: tagClass('img'),
112
- HTMLCanvasElement: tagClass('canvas'), HTMLTemplateElement: tagClass('template'),
113
- HTMLLabelElement: tagClass('label'), HTMLDivElement: tagClass('div'),
114
- HTMLSpanElement: tagClass('span'), HTMLParagraphElement: tagClass('p'),
115
- HTMLUListElement: tagClass('ul'), HTMLLIElement: tagClass('li'),
116
- HTMLBodyElement: tagClass('body'), HTMLIFrameElement: tagClass('iframe'),
117
- HTMLHeadingElement: tagClass(/^h[1-6]$/),
118
- HTMLDocument: Document, DocumentFragment, ShadowRoot: DocumentFragment,
119
- MutationObserver, DOMParser, XMLSerializer,
120
- URL: TURBO_URL, URLSearchParams,
121
- Blob: globalThis.Blob, File: TURBO_FILE, FileReader,
122
100
  customElements: makeCustomElements(),
123
- AbortController: globalThis.AbortController, AbortSignal: globalThis.AbortSignal,
124
- TextEncoder: globalThis.TextEncoder, TextDecoder: globalThis.TextDecoder,
125
- // web platform globals Node already provides
126
- fetch: globalThis.fetch ? (...a) => globalThis.fetch(...a) : undefined,
127
- Headers: globalThis.Headers, Request: globalThis.Request, Response: globalThis.Response,
128
- FormData: TurboFormData, ReadableStream: globalThis.ReadableStream,
129
- crypto: globalThis.crypto, Crypto: globalThis.Crypto, SubtleCrypto: globalThis.SubtleCrypto,
130
- btoa: (s) => Buffer.from(String(s), 'binary').toString('base64'),
131
- atob: (s) => Buffer.from(String(s), 'base64').toString('binary'),
132
- MessageChannel: globalThis.MessageChannel, MessagePort: globalThis.MessagePort,
133
- BroadcastChannel: globalThis.BroadcastChannel, EventSource: globalThis.EventSource,
134
- reportError: (e) => { /* swallow; tests assert via handlers */ void e; },
135
- requestIdleCallback: (cb) => hostSetTimeout(() => cb({ didTimeout: false, timeRemaining: () => 50 }), 0),
136
- cancelIdleCallback: (id) => hostClearTimeout(id),
137
- CSS: { supports: () => true, escape: (s) => String(s).replace(/[^a-zA-Z0-9_-]/g, (c) => '\\' + c) },
138
- XMLHttpRequest: makeXHR(),
139
101
  Image: function Image(w, h) { const img = document.createElement('img'); if (w != null) img.setAttribute('width', w); if (h != null) img.setAttribute('height', h); return img; },
140
102
  Audio: function Audio(src) { const a = document.createElement('audio'); if (src) a.setAttribute('src', src); return a; },
141
- Worker: class Worker { constructor() {} postMessage() {} terminate() {} addEventListener() {} removeEventListener() {} },
142
- // timers delegate to the captured host fns (NOT the bare names — once these
143
- // are installed on globalThis the bare names resolve back here → recursion)
144
- setTimeout: (...a) => hostSetTimeout(...a),
145
- clearTimeout: (...a) => hostClearTimeout(...a),
146
- setInterval: (...a) => hostSetInterval(...a),
147
- clearInterval: (...a) => hostClearInterval(...a),
148
- queueMicrotask: (...a) => hostQueueMicrotask(...a),
149
- structuredClone: (...a) => hostStructuredClone(...a),
150
103
  getSelection: () => document.getSelection(),
151
- scrollTo() {}, scroll() {}, scrollBy() {},
152
- // window methods libraries/tests spy on — must exist as own props for vi.spyOn
153
- open: () => null, close() {}, stop() {}, print() {}, focus() {}, blur() {},
154
- moveTo() {}, moveBy() {}, resizeTo() {}, resizeBy() {},
155
- alert() {}, confirm: () => false, prompt: () => null,
156
104
  dispatchEvent: (e) => document.dispatchEvent(e),
157
105
  addEventListener: (...a) => document.addEventListener(...a),
158
106
  removeEventListener: (...a) => document.removeEventListener(...a),
@@ -196,7 +144,8 @@ export function createWindow(document, { url = 'http://localhost/' } = {}) {
196
144
  windowProxy = new Proxy(base, {
197
145
  get(t, k) {
198
146
  if (k === 'window' || k === 'self' || k === 'globalThis' || k === 'parent' || k === 'top') return windowProxy;
199
- if (k in t) return t[k];
147
+ if (k in t) return t[k]; // per-env (incl. overrides + materialized lazy)
148
+ if (k in STATIC_BASE) return STATIC_BASE[k]; // shared stateless globals
200
149
  const factory = lazy[k];
201
150
  if (factory) {
202
151
  touched.add(k);
@@ -206,11 +155,19 @@ export function createWindow(document, { url = 'http://localhost/' } = {}) {
206
155
  }
207
156
  return undefined;
208
157
  },
209
- set(t, k, v) { t[k] = v; return true; },
158
+ set(t, k, v) { t[k] = v; return true; }, // writes to base → shadows STATIC_BASE per-env
210
159
  has(t, k) {
211
- return k in t || k in lazy ||
160
+ return k in t || k in STATIC_BASE || k in lazy ||
212
161
  k === 'window' || k === 'self' || k === 'globalThis' || k === 'parent' || k === 'top';
213
162
  },
163
+ // so vi.spyOn(window, 'scrollTo'/'open'/…) finds STATIC_BASE methods as own
164
+ // props; spyOn then defineProperty's the spy onto base (target), shadowing it.
165
+ getOwnPropertyDescriptor(t, k) {
166
+ const own = Object.getOwnPropertyDescriptor(t, k);
167
+ if (own) return own;
168
+ if (k in STATIC_BASE) return { configurable: true, enumerable: true, writable: true, value: STATIC_BASE[k] };
169
+ return undefined;
170
+ },
214
171
  });
215
172
 
216
173
  document.defaultView = windowProxy;
@@ -220,7 +177,7 @@ export function createWindow(document, { url = 'http://localhost/' } = {}) {
220
177
  // which lazy globals this test materialized (the "DOM surface used" report)
221
178
  touched: () => [...touched],
222
179
  // every global name this window can provide (for environment adapters)
223
- globalKeys: [...Object.keys(base), ...Object.keys(lazy)],
180
+ globalKeys: [...Object.keys(base), ...Object.keys(STATIC_BASE), ...Object.keys(lazy)],
224
181
  // Layer 5: drop materialized global slots, keep the class machinery warm.
225
182
  resetGlobals() {
226
183
  for (const k of touched) delete base[k];
@@ -279,3 +236,60 @@ function makeXHR() {
279
236
  }
280
237
  };
281
238
  }
239
+
240
+ // Stateless globals — identical for every window, so built ONCE at module load
241
+ // instead of per createWindow() (per test file). createWindow's Proxy falls back
242
+ // to this after its tiny per-env `base`. Nothing here captures document/url/window.
243
+ const STATIC_BASE = {
244
+ // DOM + event constructors (cheap class refs)
245
+ Node, Element, Text, Comment, Document, DocumentFragment, DocumentType, EventTarget,
246
+ Event, CustomEvent,
247
+ UIEvent, MouseEvent, PointerEvent, KeyboardEvent, InputEvent, FocusEvent,
248
+ CompositionEvent, WheelEvent, TouchEvent, DragEvent, ProgressEvent, ClipboardEvent,
249
+ DataTransfer,
250
+ // every element is a plain Element → `el instanceof HTMLElement` is true.
251
+ // Tag-specific interfaces match by localName via Symbol.hasInstance.
252
+ HTMLElement: Element, SVGElement: Element,
253
+ HTMLAnchorElement: tagClass('a'), HTMLInputElement: tagClass('input'),
254
+ HTMLTextAreaElement: tagClass('textarea'), HTMLSelectElement: tagClass('select'),
255
+ HTMLOptionElement: tagClass('option'), HTMLButtonElement: tagClass('button'),
256
+ HTMLFormElement: tagClass('form'), HTMLImageElement: tagClass('img'),
257
+ HTMLCanvasElement: tagClass('canvas'), HTMLTemplateElement: tagClass('template'),
258
+ HTMLLabelElement: tagClass('label'), HTMLDivElement: tagClass('div'),
259
+ HTMLSpanElement: tagClass('span'), HTMLParagraphElement: tagClass('p'),
260
+ HTMLUListElement: tagClass('ul'), HTMLLIElement: tagClass('li'),
261
+ HTMLBodyElement: tagClass('body'), HTMLIFrameElement: tagClass('iframe'),
262
+ HTMLHeadingElement: tagClass(/^h[1-6]$/),
263
+ HTMLDocument: Document, ShadowRoot: DocumentFragment,
264
+ MutationObserver, DOMParser, XMLSerializer,
265
+ URL: TURBO_URL, URLSearchParams,
266
+ Blob: globalThis.Blob, File: TURBO_FILE, FileReader,
267
+ AbortController: globalThis.AbortController, AbortSignal: globalThis.AbortSignal,
268
+ TextEncoder: globalThis.TextEncoder, TextDecoder: globalThis.TextDecoder,
269
+ fetch: globalThis.fetch ? (...a) => globalThis.fetch(...a) : undefined,
270
+ Headers: globalThis.Headers, Request: globalThis.Request, Response: globalThis.Response,
271
+ FormData: TurboFormData, ReadableStream: globalThis.ReadableStream,
272
+ crypto: globalThis.crypto, Crypto: globalThis.Crypto, SubtleCrypto: globalThis.SubtleCrypto,
273
+ btoa: (s) => Buffer.from(String(s), 'binary').toString('base64'),
274
+ atob: (s) => Buffer.from(String(s), 'base64').toString('binary'),
275
+ MessageChannel: globalThis.MessageChannel, MessagePort: globalThis.MessagePort,
276
+ BroadcastChannel: globalThis.BroadcastChannel, EventSource: globalThis.EventSource,
277
+ reportError: (e) => { void e; },
278
+ requestIdleCallback: (cb) => hostSetTimeout(() => cb({ didTimeout: false, timeRemaining: () => 50 }), 0),
279
+ cancelIdleCallback: (id) => hostClearTimeout(id),
280
+ CSS: { supports: () => true, escape: (s) => String(s).replace(/[^a-zA-Z0-9_-]/g, (c) => '\\' + c) },
281
+ XMLHttpRequest: makeXHR(),
282
+ Worker: class Worker { constructor() {} postMessage() {} terminate() {} addEventListener() {} removeEventListener() {} },
283
+ // timers delegate to captured host fns (NOT bare names — installed bare names
284
+ // would resolve back here → recursion)
285
+ setTimeout: (...a) => hostSetTimeout(...a),
286
+ clearTimeout: (...a) => hostClearTimeout(...a),
287
+ setInterval: (...a) => hostSetInterval(...a),
288
+ clearInterval: (...a) => hostClearInterval(...a),
289
+ queueMicrotask: (...a) => hostQueueMicrotask(...a),
290
+ structuredClone: (...a) => hostStructuredClone(...a),
291
+ scrollTo() {}, scroll() {}, scrollBy() {},
292
+ open: () => null, close() {}, stop() {}, print() {}, focus() {}, blur() {},
293
+ moveTo() {}, moveBy() {}, resizeTo() {}, resizeBy() {},
294
+ alert() {}, confirm: () => false, prompt: () => null,
295
+ };