diff --git a/.changeset/polite-pillows-dress.md b/.changeset/polite-pillows-dress.md new file mode 100644 index 000000000..8cb85efd9 --- /dev/null +++ b/.changeset/polite-pillows-dress.md @@ -0,0 +1,6 @@ +--- +"@linaria/babel-preset": patch +"@linaria/testkit": patch +--- + +feat: use happy-dom in module diff --git a/packages/babel/package.json b/packages/babel/package.json index 8b3a6d711..933cf170d 100644 --- a/packages/babel/package.json +++ b/packages/babel/package.json @@ -45,6 +45,7 @@ "@linaria/tags": "workspace:^", "@linaria/utils": "workspace:^", "cosmiconfig": "^8.0.0", + "happy-dom": "10.8.0", "source-map": "^0.7.3", "stylis": "^3.5.4" }, diff --git a/packages/babel/src/module.ts b/packages/babel/src/module.ts index 62f139180..c1eb2f12f 100644 --- a/packages/babel/src/module.ts +++ b/packages/babel/src/module.ts @@ -25,7 +25,7 @@ import type { StrictOptions } from '@linaria/utils'; import { getFileIdx } from '@linaria/utils'; import { TransformCacheCollection } from './cache'; -import * as process from './process'; +import createVmContext from './vm/createVmContext'; type HiddenModuleMembers = { _extensions: { [key: string]: () => void }; @@ -478,15 +478,7 @@ class Module { return; } - const context = vm.createContext({ - clearImmediate: NOOP, - clearInterval: NOOP, - clearTimeout: NOOP, - setImmediate: NOOP, - setInterval: NOOP, - setTimeout: NOOP, - global, - process, + const { context, teardown } = createVmContext({ module: this, exports: this.#exports, require: this.require, @@ -523,6 +515,8 @@ class Module { throw new EvalError( `${(e as Error).message} in${callstack.join('\n| ')}\n` ); + } finally { + teardown(); } } } diff --git a/packages/babel/src/vm/createVmContext.ts b/packages/babel/src/vm/createVmContext.ts new file mode 100644 index 000000000..375c5ad07 --- /dev/null +++ b/packages/babel/src/vm/createVmContext.ts @@ -0,0 +1,64 @@ +import * as vm from 'vm'; + +import { Window } from 'happy-dom'; + +import * as process from './process'; + +const NOOP = () => {}; + +function createWindow(): Window { + const win = new Window(); + + // TODO: browser doesn't expose Buffer, but a lot of dependencies use it + win.Buffer = Buffer; + win.Uint8Array = Uint8Array; + + return win; +} + +function createBaseContext( + win: Window, + additionalContext: Partial +): Partial { + const baseContext: vm.Context = win; + + baseContext.document = win.document; + baseContext.window = win; + baseContext.self = win; + baseContext.top = win; + baseContext.parent = win; + baseContext.global = win; + + baseContext.process = process; + + baseContext.clearImmediate = NOOP; + baseContext.clearInterval = NOOP; + baseContext.clearTimeout = NOOP; + baseContext.setImmediate = NOOP; + baseContext.requestAnimationFrame = NOOP; + baseContext.setInterval = NOOP; + baseContext.setTimeout = NOOP; + + // eslint-disable-next-line + for (const key in additionalContext) { + baseContext[key] = additionalContext[key]; + } + + return baseContext; +} + +function createVmContext(additionalContext: Partial) { + const window = createWindow(); + const baseContext = createBaseContext(window, additionalContext); + + const context = vm.createContext(baseContext); + + return { + context, + teardown: () => { + window.happyDOM.cancelAsync(); + }, + }; +} + +export default createVmContext; diff --git a/packages/babel/src/process.ts b/packages/babel/src/vm/process.ts similarity index 100% rename from packages/babel/src/process.ts rename to packages/babel/src/vm/process.ts diff --git a/packages/testkit/src/module.test.ts b/packages/testkit/src/module.test.ts index c25ac71ec..7e233d22e 100644 --- a/packages/testkit/src/module.test.ts +++ b/packages/testkit/src/module.test.ts @@ -244,78 +244,6 @@ it('has require.ensure available', () => { ).not.toThrow(); }); -it('has __filename available', () => { - const mod = new Module(getFileName(), '*', options); - - mod.evaluate(dedent` - module.exports = __filename; - `); - - expect(mod.exports).toBe(mod.filename); -}); - -it('has __dirname available', () => { - const mod = new Module(getFileName(), '*', options); - - mod.evaluate(dedent` - module.exports = __dirname; - `); - - expect(mod.exports).toBe(path.dirname(mod.filename)); -}); - -it('has setTimeout, clearTimeout available', () => { - const mod = new Module(getFileName(), '*', options); - - expect(() => - mod.evaluate(dedent` - const x = setTimeout(() => { - console.log('test'); - },0); - - clearTimeout(x); - `) - ).not.toThrow(); -}); - -it('has setInterval, clearInterval available', () => { - const mod = new Module(getFileName(), '*', options); - - expect(() => - mod.evaluate(dedent` - const x = setInterval(() => { - console.log('test'); - }, 1000); - - clearInterval(x); - `) - ).not.toThrow(); -}); - -it('has setImmediate, clearImmediate available', () => { - const mod = new Module(getFileName(), '*', options); - - expect(() => - mod.evaluate(dedent` - const x = setImmediate(() => { - console.log('test'); - }); - - clearImmediate(x); - `) - ).not.toThrow(); -}); - -it('has global objects available without referencing global', () => { - const mod = new Module(getFileName(), '*', options); - - expect(() => - mod.evaluate(dedent` - const x = new Set(); - `) - ).not.toThrow(); -}); - it('changes resolve behaviour on overriding _resolveFilename', () => { const resolveFilename = jest .spyOn(DefaultModuleImplementation, '_resolveFilename') @@ -385,3 +313,132 @@ it('export * compiled by typescript to commonjs works', () => { expect(mod.exports).toBe('foo'); }); + +describe('globals', () => { + it('has setTimeout, clearTimeout available', () => { + const mod = new Module(getFileName(), '*', options); + + expect(() => + mod.evaluate(dedent` + const x = setTimeout(() => { + console.log('test'); + },0); + + clearTimeout(x); + `) + ).not.toThrow(); + }); + + it('has setInterval, clearInterval available', () => { + const mod = new Module(getFileName(), '*', options); + + expect(() => + mod.evaluate(dedent` + const x = setInterval(() => { + console.log('test'); + }, 1000); + + clearInterval(x); + `) + ).not.toThrow(); + }); + + it('has setImmediate, clearImmediate available', () => { + const mod = new Module(getFileName(), '*', options); + + expect(() => + mod.evaluate(dedent` + const x = setImmediate(() => { + console.log('test'); + }); + + clearImmediate(x); + `) + ).not.toThrow(); + }); + + it('has global objects available without referencing global', () => { + const mod = new Module(getFileName(), '*', options); + + expect(() => mod.evaluate(dedent`const x = new Set();`)).not.toThrow(); + }); +}); + +describe('definable globals', () => { + it('has __filename available', () => { + const mod = new Module(getFileName(), '*', options); + + mod.evaluate(dedent` + module.exports = __filename; + `); + + expect(mod.exports).toBe(mod.filename); + }); + + it('has __dirname available', () => { + const mod = new Module(getFileName(), '*', options); + + mod.evaluate(dedent` + module.exports = __dirname; + `); + + expect(mod.exports).toBe(path.dirname(mod.filename)); + }); +}); + +describe('DOM', () => { + it('should have DOM globals available', () => { + const mod = new Module(getFileName(), '*', options); + + mod.evaluate(dedent` + module.exports = { + document: typeof document, + window: typeof window, + global: typeof global, + }; + `); + + expect(mod.exports).toEqual({ + document: 'object', + window: 'object', + global: 'object', + }); + }); + + it('should have DOM APIs available', () => { + const mod = new Module(getFileName(), '*', options); + + expect(() => + mod.evaluate(dedent` + const handler = () => {} + + document.addEventListener('click', handler); + document.removeEventListener('click', handler); + + window.addEventListener('click', handler); + window.removeEventListener('click', handler); + `) + ).not.toThrow(); + }); + + it('supports DOM manipulations', () => { + const mod = new Module(getFileName(), '*', options); + + mod.evaluate(dedent` + const el = document.createElement('div'); + el.setAttribute('id', 'test'); + + document.body.appendChild(el); + + module.exports = { + html: document.body.innerHTML, + tagName: el.tagName.toLowerCase() + }; + `); + + expect(mod.exports).toEqual({ + html: '
', + tagName: 'div', + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6217a020e..560ddda73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -377,6 +377,9 @@ importers: cosmiconfig: specifier: ^8.0.0 version: 8.0.0 + happy-dom: + specifier: 10.8.0 + version: 10.8.0 source-map: specifier: ^0.7.3 version: 0.7.3 @@ -6812,6 +6815,10 @@ packages: engines: {node: '>= 6'} dev: true + /css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + dev: false + /cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -7371,6 +7378,11 @@ packages: resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} dev: true + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: false + /envinfo@7.8.1: resolution: {integrity: sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==} engines: {node: '>=4'} @@ -9184,6 +9196,17 @@ packages: resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} dev: true + /happy-dom@10.8.0: + resolution: {integrity: sha512-ux5UfhNA9ANGf4keV7FCd9GqeQr3Bz1u9qnoPtTL0NcO1MEOeUXIUwNTB9r84Z7Q8/bsgkwi6K114zjYvnCmag==} + dependencies: + css.escape: 1.5.1 + entities: 4.5.0 + iconv-lite: 0.6.3 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + dev: false + /har-schema@2.0.0: resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} engines: {node: '>=4'} @@ -9643,6 +9666,13 @@ packages: dependencies: safer-buffer: 2.1.2 + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + /icss-utils@5.1.0(postcss@8.4.14): resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} engines: {node: ^10 || ^12 || >= 14} @@ -16351,13 +16381,18 @@ packages: dev: true /webidl-conversions@3.0.1: - resolution: {integrity: sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=} + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: true /webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} dev: true + /webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + dev: false + /webpack-cli@4.9.2(webpack-dev-server@4.9.1)(webpack@5.76.0): resolution: {integrity: sha512-m3/AACnBBzK/kMTcxWHcZFPrw/eQuY4Df1TxvIWfWM2x7mRqBQCqKEd96oCUa9jkapLBaFfRce33eGDb4Pr7YQ==} engines: {node: '>=10.13.0'} @@ -16641,6 +16676,18 @@ packages: engines: {node: '>=0.8.0'} dev: true + /whatwg-encoding@2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + dependencies: + iconv-lite: 0.6.3 + dev: false + + /whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + dev: false + /whatwg-url@5.0.0: resolution: {integrity: sha1-lmRU6HZUYuN2RNNib2dCzotwll0=} dependencies: