Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,14 @@
"scripts": {
"test": "npm run test:node",
"test:node": "node --import=tsx ./node_modules/poku/lib/bin/index.js tests --showLogs",
"test:node:none": "node --import=tsx ./node_modules/poku/lib/bin/index.js tests --showLogs --isolation=none",
"test:node:process": "node --import=tsx ./node_modules/poku/lib/bin/index.js tests --showLogs --isolation=process",
"test:bun": "bun ./node_modules/poku/lib/bin/index.js tests --showLogs",
"test:bun:none": "bun ./node_modules/poku/lib/bin/index.js tests --showLogs --isolation=none",
"test:bun:process": "bun ./node_modules/poku/lib/bin/index.js tests --showLogs --isolation=process",
"test:deno": "deno run -A npm:poku tests --showLogs",
"test:deno:none": "deno run -A npm:poku tests --showLogs --isolation=none",
"test:deno:process": "deno run -A npm:poku tests --showLogs --isolation=process",
"clean": "rimraf dist",
"build": "npm run clean && tsc -p tsconfig.dist.json",
"typecheck": "tsc -p tsconfig.build.json --noEmit",
Expand Down Expand Up @@ -56,15 +62,11 @@
"provenance": true
},
"peerDependencies": {
"@happy-dom/global-registrator": ">=20",
"happy-dom": ">=20",
"jsdom": ">=22",
"poku": ">=4.1.0"
},
"peerDependenciesMeta": {
"@happy-dom/global-registrator": {
"optional": true
},
"happy-dom": {
"optional": true
},
Expand Down
56 changes: 40 additions & 16 deletions src/dom-env.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GlobalRegistrator } from '@happy-dom/global-registrator';
import { GlobalWindow } from 'happy-dom';
import type { RuntimeOptions } from './types.ts';

type SetupDomEnvironmentOptions = {
Expand All @@ -19,31 +19,55 @@ const applyReactActEnvironment = (enabled: boolean) => {
}
};

const defineGlobal = (key: keyof typeof globalThis, value: unknown) => {
Object.defineProperty(globalThis, key, {
configurable: true,
writable: true,
value,
});
};

export const setupHappyDomEnvironment = async (
options: SetupDomEnvironmentOptions
) => {
if (!globalThis.window || !globalThis.document) {
GlobalRegistrator.register({
url: options.runtimeOptions.domUrl,
});
// Save native event primitives before installing Happy DOM globals.
// Deno's runtime calls globalThis.dispatchEvent('beforeunload') on exit
// using its own native Event constructor — if Happy DOM's dispatchEvent
// is installed instead, the type check inside it throws a TypeError.
const existingDispatchEvent = globalThis.dispatchEvent;
const existingEvent = globalThis.Event;
const existingCustomEvent = globalThis.CustomEvent;

const happyWindow = new GlobalWindow({ url: options.runtimeOptions.domUrl });

const nativeDispatchEvent = globalThis.window.dispatchEvent;
if (typeof nativeDispatchEvent === 'function') {
globalThis.dispatchEvent = nativeDispatchEvent.bind(globalThis.window);
defineGlobal('window', happyWindow);
defineGlobal('document', happyWindow.document);
defineGlobal('navigator', happyWindow.navigator);
defineGlobal('HTMLElement', happyWindow.HTMLElement);
defineGlobal('Element', happyWindow.Element);
defineGlobal('Node', happyWindow.Node);
defineGlobal('Text', happyWindow.Text);
defineGlobal('SVGElement', happyWindow.SVGElement);
defineGlobal('MutationObserver', happyWindow.MutationObserver);
defineGlobal('requestAnimationFrame', happyWindow.requestAnimationFrame);
defineGlobal('cancelAnimationFrame', happyWindow.cancelAnimationFrame);

// Prefer the runtime's native Event constructors so that Deno's internal
// event dispatch (e.g. beforeunload) continues to work correctly.
defineGlobal('Event', typeof existingEvent === 'function' ? existingEvent : happyWindow.Event);
defineGlobal('CustomEvent', typeof existingCustomEvent === 'function' ? existingCustomEvent : happyWindow.CustomEvent);

if (typeof existingDispatchEvent === 'function') {
globalThis.dispatchEvent = existingDispatchEvent;
} else {
globalThis.dispatchEvent = happyWindow.dispatchEvent.bind(happyWindow) as unknown as typeof globalThis.dispatchEvent;
}
}

applyReactActEnvironment(Boolean(options.enableReactActEnvironment));
};

const defineGlobal = (key: keyof typeof globalThis, value: unknown) => {
Object.defineProperty(globalThis, key, {
configurable: true,
writable: true,
value,
});
};

export const setupJsdomEnvironment = async (
options: SetupDomEnvironmentOptions
) => {
Expand All @@ -64,7 +88,7 @@ export const setupJsdomEnvironment = async (
pretendToBeVisual: true,
});

defineGlobal('window', dom.window as unknown as Window & typeof globalThis);
defineGlobal('window', dom.window);
defineGlobal('document', dom.window.document);
defineGlobal('navigator', dom.window.navigator);
defineGlobal('HTMLElement', dom.window.HTMLElement);
Expand Down
17 changes: 15 additions & 2 deletions src/plugin-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ type TsxEsmApiModule = {

const TSX_LOADER_MODULE = 'tsx/esm/api';

// Once tsx is registered in a Node.js process it cannot be safely deregistered
// and re-registered (tsx's hook worker re-instantiation fails with an invalid
// URL scheme). Under isolation:'none' everything runs in the same process for
// its lifetime, so keeping the loader registered permanently is correct.
const TSX_LOADER_REGISTERED_KEY = Symbol.for('@pokujs/dom.tsx-loader-registered');
type GlobalWithTsxFlag = typeof globalThis & { [TSX_LOADER_REGISTERED_KEY]?: boolean };

const appendMissingRuntimeArgs = (runtimeOptionArgs: string[]) => {
for (const arg of runtimeOptionArgs) {
if (process.argv.includes(arg)) continue;
Expand All @@ -19,7 +26,11 @@ const loadDomSetupModule = async (domSetupPath: string) => {
await import(pathToFileURL(domSetupPath).href);
};

const registerNodeTsxLoader = async (packageTag: string) => {
const registerNodeTsxLoader = async (packageTag: string): Promise<() => void> => {
const g = globalThis as GlobalWithTsxFlag;

if (g[TSX_LOADER_REGISTERED_KEY]) return () => {};

const requireFromCwd = createRequire(`${process.cwd()}/`);

try {
Expand All @@ -29,7 +40,9 @@ const registerNodeTsxLoader = async (packageTag: string) => {
throw new Error('Missing register() export from tsx loader API');
}

return mod.register();
mod.register();
g[TSX_LOADER_REGISTERED_KEY] = true;
return () => {};
} catch (error) {
throw new Error(
`[${packageTag}] isolation "none" in Node.js requires a working "tsx" installation to load .tsx/.jsx test files.`,
Expand Down
Loading