Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BREAKING CHANGE: [#320] Adds support for ECMAScript modules #1705

Merged
merged 15 commits into from
Feb 5, 2025
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
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
**/tmp
**/lib
**/cjs
**/tmp
**/tmp
**/test/**/modules-with-compilation-error
12 changes: 12 additions & 0 deletions packages/happy-dom/src/PropertySymbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,3 +379,15 @@ export const root = Symbol('root');
export const filterNode = Symbol('filterNode');
export const customElementReactionStack = Symbol('customElementReactionStack');
export const dispatching = Symbol('dispatching');
export const modules = Symbol('modules');
export const preloads = Symbol('preloads');
export const body = Symbol('body');
export const redirect = Symbol('redirect');
export const referrerPolicy = Symbol('referrerPolicy');
export const signal = Symbol('signal');
export const bodyUsed = Symbol('bodyUsed');
export const credentials = Symbol('credentials');
export const blocking = Symbol('blocking');
export const moduleImportMap = Symbol('moduleImportMap');
export const dispatchError = Symbol('dispatchError');
export const supports = Symbol('supports');
100 changes: 81 additions & 19 deletions packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@ export default class AsyncTaskManager {
private runningTaskCount = 0;
private runningTimers: NodeJS.Timeout[] = [];
private runningImmediates: NodeJS.Immediate[] = [];
private debugTrace: Map<number | NodeJS.Timeout | NodeJS.Immediate, string> = new Map();
private waitUntilCompleteTimer: NodeJS.Timeout | null = null;
private waitUntilCompleteResolvers: Array<() => void> = [];
private waitUntilCompleteResolvers: Array<{
resolve: () => void;
reject: (error: Error) => void;
}> = [];
private aborted = false;
private destroyed = false;
#browserFrame: IBrowserFrame;
#debugTimeout: NodeJS.Timeout | null;

/**
* Constructor.
Expand All @@ -37,8 +42,8 @@ export default class AsyncTaskManager {
* @returns Promise.
*/
public waitUntilComplete(): Promise<void> {
return new Promise((resolve) => {
this.waitUntilCompleteResolvers.push(resolve);
return new Promise((resolve, reject) => {
this.waitUntilCompleteResolvers.push({ resolve, reject });
this.resolveWhenComplete();
});
}
Expand All @@ -48,8 +53,8 @@ export default class AsyncTaskManager {
*/
public abort(): Promise<void> {
if (this.aborted) {
return new Promise((resolve) => {
this.waitUntilCompleteResolvers.push(resolve);
return new Promise((resolve, reject) => {
this.waitUntilCompleteResolvers.push({ resolve, reject });
this.resolveWhenComplete();
});
}
Expand All @@ -61,8 +66,8 @@ export default class AsyncTaskManager {
*/
public destroy(): Promise<void> {
if (this.aborted) {
return new Promise((resolve) => {
this.waitUntilCompleteResolvers.push(resolve);
return new Promise((resolve, reject) => {
this.waitUntilCompleteResolvers.push({ resolve, reject });
this.resolveWhenComplete();
});
}
Expand All @@ -84,6 +89,9 @@ export default class AsyncTaskManager {
this.waitUntilCompleteTimer = null;
}
this.runningTimers.push(timerID);
if (this.#browserFrame.page?.context?.browser?.settings?.debug?.traceWaitUntilComplete > 0) {
this.debugTrace.set(timerID, new Error().stack);
}
}

/**
Expand All @@ -99,9 +107,10 @@ export default class AsyncTaskManager {
const index = this.runningTimers.indexOf(timerID);
if (index !== -1) {
this.runningTimers.splice(index, 1);
if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) {
this.resolveWhenComplete();
}
this.resolveWhenComplete();
}
if (this.#browserFrame.page?.context?.browser?.settings?.debug?.traceWaitUntilComplete > 0) {
this.debugTrace.delete(timerID);
}
}

Expand All @@ -120,6 +129,9 @@ export default class AsyncTaskManager {
this.waitUntilCompleteTimer = null;
}
this.runningImmediates.push(immediateID);
if (this.#browserFrame.page?.context?.browser?.settings?.debug?.traceWaitUntilComplete > 0) {
this.debugTrace.set(immediateID, new Error().stack);
}
}

/**
Expand All @@ -135,9 +147,10 @@ export default class AsyncTaskManager {
const index = this.runningImmediates.indexOf(immediateID);
if (index !== -1) {
this.runningImmediates.splice(index, 1);
if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) {
this.resolveWhenComplete();
}
this.resolveWhenComplete();
}
if (this.#browserFrame.page?.context?.browser?.settings?.debug?.traceWaitUntilComplete > 0) {
this.debugTrace.delete(immediateID);
}
}

Expand All @@ -163,6 +176,9 @@ export default class AsyncTaskManager {
const taskID = this.newTaskID();
this.runningTasks[taskID] = abortHandler ? abortHandler : () => {};
this.runningTaskCount++;
if (this.#browserFrame.page?.context?.browser?.settings?.debug?.traceWaitUntilComplete > 0) {
this.debugTrace.set(taskID, new Error().stack);
}
return taskID;
}

Expand All @@ -178,9 +194,10 @@ export default class AsyncTaskManager {
if (this.runningTasks[taskID]) {
delete this.runningTasks[taskID];
this.runningTaskCount--;
if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) {
this.resolveWhenComplete();
}
this.resolveWhenComplete();
}
if (this.#browserFrame.page?.context?.browser?.settings?.debug?.traceWaitUntilComplete > 0) {
this.debugTrace.delete(taskID);
}
}

Expand All @@ -207,6 +224,8 @@ export default class AsyncTaskManager {
* Resolves when complete.
*/
private resolveWhenComplete(): void {
this.applyDebugging();

if (this.runningTaskCount || this.runningTimers.length || this.runningImmediates.length) {
return;
}
Expand All @@ -222,16 +241,58 @@ export default class AsyncTaskManager {
this.waitUntilCompleteTimer = TIMER.setTimeout(() => {
this.waitUntilCompleteTimer = null;
if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) {
if (this.#debugTimeout) {
TIMER.clearTimeout(this.#debugTimeout);
}
const resolvers = this.waitUntilCompleteResolvers;
this.waitUntilCompleteResolvers = [];
for (const resolver of resolvers) {
resolver();
resolver.resolve();
}
this.aborted = false;
} else {
this.applyDebugging();
}
}, 1);
}

/**
* Applies debugging.
*/
private applyDebugging(): void {
const debug = this.#browserFrame.page?.context?.browser?.settings?.debug;
if (!debug?.traceWaitUntilComplete || debug.traceWaitUntilComplete < 1) {
return;
}
if (this.#debugTimeout) {
return;
}
this.#debugTimeout = TIMER.setTimeout(() => {
this.#debugTimeout = null;

let errorMessage = `The maximum time was reached for "waitUntilComplete()".\n\n${
this.debugTrace.size
} task${
this.debugTrace.size === 1 ? '' : 's'
} did not end in time.\n\nThe following traces were recorded:\n\n`;

for (const [key, value] of this.debugTrace.entries()) {
const type = typeof key === 'number' ? 'Task' : 'Timer';
errorMessage += `${type} #${key}\n‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾${value
.replace(/Error:/, '')
.replace(/\s+at /gm, '\n> ')}\n\n`;
}

const error = new Error(errorMessage);

for (const resolver of this.waitUntilCompleteResolvers) {
resolver.reject(error);
}

this.abortAll(true);
}, debug.traceWaitUntilComplete);
}

/**
* Aborts all tasks.
*
Expand All @@ -248,6 +309,7 @@ export default class AsyncTaskManager {
this.runningTaskCount = 0;
this.runningImmediates = [];
this.runningTimers = [];
this.debugTrace = new Map();

if (this.waitUntilCompleteTimer) {
TIMER.clearTimeout(this.waitUntilCompleteTimer);
Expand All @@ -267,8 +329,8 @@ export default class AsyncTaskManager {
}

// We need to wait for microtasks to complete before resolving.
return new Promise((resolve) => {
this.waitUntilCompleteResolvers.push(resolve);
return new Promise((resolve, reject) => {
this.waitUntilCompleteResolvers.push({ resolve, reject });
this.resolveWhenComplete();
});
}
Expand Down
4 changes: 4 additions & 0 deletions packages/happy-dom/src/browser/BrowserSettingsFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export default class BrowserSettingsFactory {
device: {
...DefaultBrowserSettings.device,
...settings?.device
},
debug: {
...DefaultBrowserSettings.debug,
...settings?.debug
}
};
}
Expand Down
3 changes: 3 additions & 0 deletions packages/happy-dom/src/browser/DefaultBrowserSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,8 @@ export default <IBrowserSettings>{
device: {
prefersColorScheme: 'light',
mediaType: 'screen'
},
debug: {
traceWaitUntilComplete: -1
}
};
7 changes: 7 additions & 0 deletions packages/happy-dom/src/browser/types/IBrowserSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,11 @@ export default interface IBrowserSettings {
prefersColorScheme: string;
mediaType: string;
};

/**
* Debug settings.
*/
debug: {
traceWaitUntilComplete: number;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,11 @@ export default interface IOptionalBrowserSettings {
prefersColorScheme?: string;
mediaType?: string;
};

/**
* Debug settings.
*/
debug?: {
traceWaitUntilComplete?: number;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import IBrowserFrame from '../types/IBrowserFrame.js';
import * as PropertySymbol from '../../PropertySymbol.js';
import IGoToOptions from '../types/IGoToOptions.js';
import Response from '../../fetch/Response.js';
import DocumentReadyStateManager from '../../nodes/document/DocumentReadyStateManager.js';
import BrowserWindow from '../../window/BrowserWindow.js';
import BrowserFrameFactory from './BrowserFrameFactory.js';
import BrowserFrameURL from './BrowserFrameURL.js';
Expand Down Expand Up @@ -61,9 +60,7 @@ export default class BrowserFrameNavigator {
// Javascript protocol
if (targetURL.protocol === 'javascript:') {
if (frame && !frame.page.context.browser.settings.disableJavaScriptEvaluation) {
const readyStateManager = (<
{ [PropertySymbol.readyStateManager]: DocumentReadyStateManager }
>(<unknown>frame.window))[PropertySymbol.readyStateManager];
const readyStateManager = frame.window[PropertySymbol.readyStateManager];

readyStateManager.startTask();
const code =
Expand Down Expand Up @@ -178,9 +175,7 @@ export default class BrowserFrameNavigator {
}

// Start navigation
const readyStateManager = (<{ [PropertySymbol.readyStateManager]: DocumentReadyStateManager }>(
(<unknown>frame.window)
))[PropertySymbol.readyStateManager];
const readyStateManager = frame.window[PropertySymbol.readyStateManager];
const abortController = new frame.window.AbortController();
const timeout = frame.window.setTimeout(
() => abortController.abort(new Error('Request timed out.')),
Expand Down
16 changes: 12 additions & 4 deletions packages/happy-dom/src/dom/DOMTokenList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,29 @@ export default class DOMTokenList {
items: [],
attributeValue: ''
};
private [PropertySymbol.supports]: string[];

/**
* Constructor.
*
* @param illegalConstructorSymbol Illegal constructor symbol.
* @param ownerElement Owner element.
* @param attributeName Attribute name.
* @param [supports] Supports.
*/
constructor(illegalConstructorSymbol: symbol, ownerElement: Element, attributeName: string) {
constructor(
illegalConstructorSymbol: symbol,
ownerElement: Element,
attributeName: string,
supports?: string[]
) {
if (illegalConstructorSymbol !== PropertySymbol.illegalConstructor) {
throw new TypeError('Illegal constructor');
}

this[PropertySymbol.ownerElement] = ownerElement;
this[PropertySymbol.attributeName] = attributeName;
this[PropertySymbol.supports] = supports || [];

const methodBinder = new ClassMethodBinder(this, [DOMTokenList]);

Expand Down Expand Up @@ -189,10 +197,10 @@ export default class DOMTokenList {
/**
* Supports.
*
* @param _token Token.
* @param token Token.
*/
public supports(_token: string): boolean {
return false;
public supports(token: string): boolean {
return this[PropertySymbol.supports].includes(token);
}

/**
Expand Down
Loading