UNPKG

lettersanitizer

Version:

DOM-based HTML email sanitizer for in-browser email rendering.

170 lines (160 loc) 4.75 kB
import { Environment } from 'vitest'; import { populateGlobal } from 'vitest/environments'; function catchWindowErrors(window: Window) { let userErrorListenerCount = 0; function throwUnhandlerError(e: ErrorEvent) { if (userErrorListenerCount === 0 && e.error != null) { process.emit('uncaughtException', e.error); } } const addEventListener = window.addEventListener.bind(window); const removeEventListener = window.removeEventListener.bind(window); window.addEventListener('error', throwUnhandlerError); window.addEventListener = function (...args: [any, any, any]) { if (args[0] === 'error') { userErrorListenerCount++; } return addEventListener.apply(this, args); }; window.removeEventListener = function (...args: [any, any, any]) { if (args[0] === 'error' && userErrorListenerCount) { userErrorListenerCount--; } return removeEventListener.apply(this, args); }; return function clearErrorHandlers() { window.removeEventListener('error', throwUnhandlerError); }; } export default <Environment>{ name: 'jsdom', transformMode: 'web', async setupVM({ jsdom = {} }) { const { CookieJar, JSDOM, ResourceLoader, VirtualConsole } = await import( 'jsdom' ); const { html = '<!DOCTYPE html>', userAgent, url = 'http://localhost:3000', contentType = 'text/html', pretendToBeVisual = true, includeNodeLocations = false, runScripts = 'dangerously', resources, console = false, cookieJar = false, ...restOptions } = jsdom as any; let dom = new JSDOM(html, { pretendToBeVisual, resources: resources ?? (userAgent ? new ResourceLoader({ userAgent }) : undefined), runScripts, url, virtualConsole: console && globalThis.console ? new VirtualConsole().sendTo(globalThis.console) : undefined, cookieJar: cookieJar ? new CookieJar() : undefined, includeNodeLocations, contentType, userAgent, ...restOptions, }); const clearWindowErrors = catchWindowErrors(dom.window as any); // TODO: browser doesn't expose Buffer, but a lot of dependencies use it dom.window.Buffer = Buffer; dom.window.jsdom = dom; // inject web globals if they missing in JSDOM but otherwise available in Nodejs // https://nodejs.org/dist/latest/docs/api/globals.html const globalNames = [ 'structuredClone', 'fetch', 'Request', 'Response', 'BroadcastChannel', 'MessageChannel', 'MessagePort', 'TextEncoder', 'TextDecoder', ] as const; for (const name of globalNames) { const value = globalThis[name]; if ( typeof value !== 'undefined' && typeof dom.window[name] === 'undefined' ) { dom.window[name] = value; } } return { getVmContext() { return dom.getInternalVMContext(); }, teardown() { clearWindowErrors(); dom.window.close(); dom = undefined as any; }, }; }, async setup(global, { jsdom = {} }) { const { CookieJar, JSDOM, ResourceLoader, VirtualConsole } = await import( 'jsdom' ); const { html = '<!DOCTYPE html>', userAgent, url = 'http://localhost:3000', contentType = 'text/html', pretendToBeVisual = true, includeNodeLocations = false, runScripts = 'dangerously', resources, console = false, cookieJar = false, ...restOptions } = jsdom as any; const dom = new JSDOM(html, { pretendToBeVisual, resources: resources ?? (userAgent ? new ResourceLoader({ userAgent }) : undefined), runScripts, url, virtualConsole: console && global.console ? new VirtualConsole().sendTo(global.console) : undefined, cookieJar: cookieJar ? new CookieJar() : undefined, includeNodeLocations, contentType, userAgent, ...restOptions, }); const { keys, originals } = populateGlobal(global, dom.window, { bindFunctions: true, }); const clearWindowErrors = catchWindowErrors(global); global.jsdom = dom; dom.window.DOMParser = class { parseFromString( html: string, mimeType: DOMParserSupportedType, ): Document { return new JSDOM(html, { contentType: mimeType }).window.document; } }; return { teardown(global) { clearWindowErrors(); dom.window.close(); delete global.jsdom; keys.forEach(key => delete global[key]); originals.forEach((v, k) => (global[k] = v)); }, }; }, };