From 3f89b254c0c4d2ba80341aa356234f343fa710b6 Mon Sep 17 00:00:00 2001 From: Tim Seckinger Date: Mon, 27 Apr 2020 23:45:05 +0200 Subject: [PATCH 1/7] basic jest-runtime require.resolve test --- .../__tests__/runtime_require_resolve.test.ts | 47 +++++++++++++++++++ .../src/__tests__/test_root/resolve_mapped.js | 10 ++++ .../src/__tests__/test_root/resolve_self.js | 10 ++++ 3 files changed, 67 insertions(+) create mode 100644 packages/jest-runtime/src/__tests__/runtime_require_resolve.test.ts create mode 100644 packages/jest-runtime/src/__tests__/test_root/resolve_mapped.js create mode 100644 packages/jest-runtime/src/__tests__/test_root/resolve_self.js diff --git a/packages/jest-runtime/src/__tests__/runtime_require_resolve.test.ts b/packages/jest-runtime/src/__tests__/runtime_require_resolve.test.ts new file mode 100644 index 000000000000..97298fbfc326 --- /dev/null +++ b/packages/jest-runtime/src/__tests__/runtime_require_resolve.test.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +import type {Config} from '@jest/types'; +import type Runtime from '..'; + +let createRuntime: ( + path: string, + config?: Config.InitialOptions, +) => Promise; + +describe('Runtime require.resolve', () => { + beforeEach(() => { + createRuntime = require('createRuntime'); + }); + + it('resolves a module path', async () => { + const runtime = await createRuntime(__filename); + const resolved = runtime.requireModule( + runtime.__mockRootPath, + './resolve_self.js', + ); + expect(resolved).toEqual(require.resolve('./test_root/resolve_self.js')); + }); + + it('resolves a module path with moduleNameMapper', async () => { + const runtime = await createRuntime(__filename, { + moduleNameMapper: { + '^testMapped/(.*)': '/mapped_dir/$1', + }, + }); + const resolved = runtime.requireModule( + runtime.__mockRootPath, + './resolve_mapped.js', + ); + expect(resolved).toEqual( + require.resolve('./test_root/mapped_dir/moduleInMapped.js'), + ); + }); +}); diff --git a/packages/jest-runtime/src/__tests__/test_root/resolve_mapped.js b/packages/jest-runtime/src/__tests__/test_root/resolve_mapped.js new file mode 100644 index 000000000000..377e18fcaa0f --- /dev/null +++ b/packages/jest-runtime/src/__tests__/test_root/resolve_mapped.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +module.exports = require.resolve('testMapped/moduleInMapped'); diff --git a/packages/jest-runtime/src/__tests__/test_root/resolve_self.js b/packages/jest-runtime/src/__tests__/test_root/resolve_self.js new file mode 100644 index 000000000000..63195879cf57 --- /dev/null +++ b/packages/jest-runtime/src/__tests__/test_root/resolve_self.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +module.exports = require.resolve('./resolve_self'); From eb5b9d318632e6635c508ea39aae411fd4dc33a4 Mon Sep 17 00:00:00 2001 From: Tim Seckinger Date: Tue, 28 Apr 2020 00:37:27 +0200 Subject: [PATCH 2/7] implement require.resolve option outsideJestVm for internal modules only --- .../__tests__/runtime_require_resolve.test.ts | 21 ++++++++ .../test_root/resolve_and_require_outside.js | 14 +++++ packages/jest-runtime/src/index.ts | 52 +++++++++++++++++-- 3 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 packages/jest-runtime/src/__tests__/test_root/resolve_and_require_outside.js diff --git a/packages/jest-runtime/src/__tests__/runtime_require_resolve.test.ts b/packages/jest-runtime/src/__tests__/runtime_require_resolve.test.ts index 97298fbfc326..9e80bd4577c2 100644 --- a/packages/jest-runtime/src/__tests__/runtime_require_resolve.test.ts +++ b/packages/jest-runtime/src/__tests__/runtime_require_resolve.test.ts @@ -44,4 +44,25 @@ describe('Runtime require.resolve', () => { require.resolve('./test_root/mapped_dir/moduleInMapped.js'), ); }); + + describe('with the outsideJestVm option', () => { + it('forwards to the real Node require in an internal context', async () => { + const runtime = await createRuntime(__filename); + const module = runtime.requireInternalModule( + runtime.__mockRootPath, + './resolve_and_require_outside.js', + ); + expect(module).toBe(require('./test_root/create_require_module')); + }); + + it('ignores the option in an external context', async () => { + const runtime = await createRuntime(__filename); + const module = runtime.requireModule( + runtime.__mockRootPath, + './resolve_and_require_outside.js', + ); + expect(module.foo).toBe('foo'); + expect(module).not.toBe(require('./test_root/create_require_module')); + }); + }); }); diff --git a/packages/jest-runtime/src/__tests__/test_root/resolve_and_require_outside.js b/packages/jest-runtime/src/__tests__/test_root/resolve_and_require_outside.js new file mode 100644 index 000000000000..97b015d82312 --- /dev/null +++ b/packages/jest-runtime/src/__tests__/test_root/resolve_and_require_outside.js @@ -0,0 +1,14 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +const path = require.resolve('./create_require_module', {outsideJestVm: true}); +if (typeof path !== 'string') { + throw new Error('require.resolve not spec-compliant: must return a string'); +} +module.exports = require(path); diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index ecb2d25f3ef8..3ec15b550018 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -81,7 +81,11 @@ const defaultTransformOptions: InternalModuleOptions = { type InitialModule = Partial & Pick; type ModuleRegistry = Map; -type ResolveOptions = Parameters[1]; + +type ResolveOptions = Parameters[1] & { + outsideJestVm?: true; +}; +const OUTSIDE_JEST_VM_PROTOCOL = 'outside-jest-vm:'; type BooleanObject = Record; type CacheFS = {[path: string]: string}; @@ -534,6 +538,13 @@ class Runtime { } requireInternalModule(from: Config.Path, to?: string): T { + if (to) { + const outsideJestVmPath = this._decodePossibleOutsideJestVmPath(to); + if (outsideJestVmPath) { + return require(outsideJestVmPath); + } + } + return this.requireModule(from, to, { isInternalModule: true, supportsDynamicImport: false, @@ -833,6 +844,31 @@ class Runtime { this._moduleMocker.clearAllMocks(); } + // fileUrl.protocol cannot be set to a non-standard protocol, so we use string manipulation + private _createOutsideJestVmPath(path: string) { + return pathToFileURL(path) + .toString() + .replace(/^file:/, OUTSIDE_JEST_VM_PROTOCOL); + } + private _decodePossibleOutsideJestVmPath( + maybeUrl: string, + ): string | undefined { + let url: URL; + try { + url = new URL(maybeUrl); + } catch { + return undefined; + } + if (url.protocol !== OUTSIDE_JEST_VM_PROTOCOL) { + return undefined; + } + return fileURLToPath( + url + .toString() + .replace(new RegExp('^' + OUTSIDE_JEST_VM_PROTOCOL), 'file:'), + ); + } + private _resolveModule(from: Config.Path, to?: string) { return to ? this._resolver.resolveModule(from, to) : from; } @@ -1269,9 +1305,17 @@ class Runtime { from: InitialModule, options?: InternalModuleOptions, ): LocalModuleRequire { - // TODO: somehow avoid having to type the arguments - they should come from `NodeRequire/LocalModuleRequire.resolve` - const resolve = (moduleName: string, options: ResolveOptions) => - this._requireResolve(from.filename, moduleName, options); + const resolve = (moduleName: string, resolveOptions?: ResolveOptions) => { + const resolved = this._requireResolve( + from.filename, + moduleName, + resolveOptions, + ); + if (resolveOptions?.outsideJestVm && options?.isInternalModule) { + return this._createOutsideJestVmPath(resolved); + } + return resolved; + }; resolve.paths = (moduleName: string) => this._requireResolvePaths(from.filename, moduleName); From e141acebddb62195e121d3cfe67624f555d042fd Mon Sep 17 00:00:00 2001 From: Tim Seckinger Date: Tue, 28 Apr 2020 14:28:15 +0200 Subject: [PATCH 3/7] review improvements --- .../__tests__/runtime_require_resolve.test.ts | 4 +-- packages/jest-runtime/src/helpers.ts | 20 +++++++++++ packages/jest-runtime/src/index.ts | 36 ++++--------------- 3 files changed, 28 insertions(+), 32 deletions(-) diff --git a/packages/jest-runtime/src/__tests__/runtime_require_resolve.test.ts b/packages/jest-runtime/src/__tests__/runtime_require_resolve.test.ts index 9e80bd4577c2..ce8a48ee7c12 100644 --- a/packages/jest-runtime/src/__tests__/runtime_require_resolve.test.ts +++ b/packages/jest-runtime/src/__tests__/runtime_require_resolve.test.ts @@ -6,8 +6,6 @@ * */ -'use strict'; - import type {Config} from '@jest/types'; import type Runtime from '..'; @@ -57,7 +55,7 @@ describe('Runtime require.resolve', () => { it('ignores the option in an external context', async () => { const runtime = await createRuntime(__filename); - const module = runtime.requireModule( + const module = runtime.requireModule( runtime.__mockRootPath, './resolve_and_require_outside.js', ); diff --git a/packages/jest-runtime/src/helpers.ts b/packages/jest-runtime/src/helpers.ts index fb42e2b1e4ca..efda283cd1a3 100644 --- a/packages/jest-runtime/src/helpers.ts +++ b/packages/jest-runtime/src/helpers.ts @@ -10,6 +10,26 @@ import slash = require('slash'); import glob = require('glob'); import type {Config} from '@jest/types'; +const OUTSIDE_JEST_VM_PROTOCOL = 'jest-main:'; +// String manipulation is easier here, fileURLToPath is only in newer Nodes, +// plus setting non-standard protocols on URL objects is difficult. +export const createOutsideJestVmPath = (path: string) => { + return `${OUTSIDE_JEST_VM_PROTOCOL}//` + encodeURIComponent(path); +}; +export const decodePossibleOutsideJestVmPath = ( + outsideJestVmPath: string, +): string | undefined => { + if (outsideJestVmPath.startsWith(OUTSIDE_JEST_VM_PROTOCOL)) { + return decodeURIComponent( + outsideJestVmPath.replace( + new RegExp('^' + OUTSIDE_JEST_VM_PROTOCOL + '//'), + '', + ), + ); + } + return undefined; +}; + export const findSiblingsWithFileExtension = ( moduleFileExtensions: Config.ProjectConfig['moduleFileExtensions'], from: Config.Path, diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 3ec15b550018..c9ba7598a485 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -45,7 +45,11 @@ import {CoverageInstrumenter, V8Coverage} from 'collect-v8-coverage'; import * as fs from 'graceful-fs'; import {run as cliRun} from './cli'; import {options as cliOptions} from './cli/args'; -import {findSiblingsWithFileExtension} from './helpers'; +import { + findSiblingsWithFileExtension, + decodePossibleOutsideJestVmPath, + createOutsideJestVmPath, +} from './helpers'; import type {Context as JestContext} from './types'; import jestMock = require('jest-mock'); import HasteMap = require('jest-haste-map'); @@ -85,7 +89,6 @@ type ModuleRegistry = Map; type ResolveOptions = Parameters[1] & { outsideJestVm?: true; }; -const OUTSIDE_JEST_VM_PROTOCOL = 'outside-jest-vm:'; type BooleanObject = Record; type CacheFS = {[path: string]: string}; @@ -539,7 +542,7 @@ class Runtime { requireInternalModule(from: Config.Path, to?: string): T { if (to) { - const outsideJestVmPath = this._decodePossibleOutsideJestVmPath(to); + const outsideJestVmPath = decodePossibleOutsideJestVmPath(to); if (outsideJestVmPath) { return require(outsideJestVmPath); } @@ -844,31 +847,6 @@ class Runtime { this._moduleMocker.clearAllMocks(); } - // fileUrl.protocol cannot be set to a non-standard protocol, so we use string manipulation - private _createOutsideJestVmPath(path: string) { - return pathToFileURL(path) - .toString() - .replace(/^file:/, OUTSIDE_JEST_VM_PROTOCOL); - } - private _decodePossibleOutsideJestVmPath( - maybeUrl: string, - ): string | undefined { - let url: URL; - try { - url = new URL(maybeUrl); - } catch { - return undefined; - } - if (url.protocol !== OUTSIDE_JEST_VM_PROTOCOL) { - return undefined; - } - return fileURLToPath( - url - .toString() - .replace(new RegExp('^' + OUTSIDE_JEST_VM_PROTOCOL), 'file:'), - ); - } - private _resolveModule(from: Config.Path, to?: string) { return to ? this._resolver.resolveModule(from, to) : from; } @@ -1312,7 +1290,7 @@ class Runtime { resolveOptions, ); if (resolveOptions?.outsideJestVm && options?.isInternalModule) { - return this._createOutsideJestVmPath(resolved); + return createOutsideJestVmPath(resolved); } return resolved; }; From faed0420950121b65cdc884926f83596bf8a34f6 Mon Sep 17 00:00:00 2001 From: Tim Seckinger Date: Tue, 28 Apr 2020 14:30:39 +0200 Subject: [PATCH 4/7] lint --- packages/jest-runtime/src/helpers.ts | 5 ++--- packages/jest-runtime/src/index.ts | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/jest-runtime/src/helpers.ts b/packages/jest-runtime/src/helpers.ts index efda283cd1a3..4a7d28aa7f27 100644 --- a/packages/jest-runtime/src/helpers.ts +++ b/packages/jest-runtime/src/helpers.ts @@ -13,9 +13,8 @@ import type {Config} from '@jest/types'; const OUTSIDE_JEST_VM_PROTOCOL = 'jest-main:'; // String manipulation is easier here, fileURLToPath is only in newer Nodes, // plus setting non-standard protocols on URL objects is difficult. -export const createOutsideJestVmPath = (path: string) => { - return `${OUTSIDE_JEST_VM_PROTOCOL}//` + encodeURIComponent(path); -}; +export const createOutsideJestVmPath = (path: string): string => + OUTSIDE_JEST_VM_PROTOCOL + '//' + encodeURIComponent(path); export const decodePossibleOutsideJestVmPath = ( outsideJestVmPath: string, ): string | undefined => { diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index c9ba7598a485..b2cefbfeb2ec 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -46,9 +46,9 @@ import * as fs from 'graceful-fs'; import {run as cliRun} from './cli'; import {options as cliOptions} from './cli/args'; import { - findSiblingsWithFileExtension, - decodePossibleOutsideJestVmPath, createOutsideJestVmPath, + decodePossibleOutsideJestVmPath, + findSiblingsWithFileExtension, } from './helpers'; import type {Context as JestContext} from './types'; import jestMock = require('jest-mock'); From a2a05bdd34180573103b291542e97a60980659e4 Mon Sep 17 00:00:00 2001 From: Tim Seckinger Date: Tue, 28 Apr 2020 18:46:46 +0200 Subject: [PATCH 5/7] use Symbol key for resolve option This means other tools (e.g. Yarn PnP) should ignore the unknown option --- .../src/__tests__/runtime_require_resolve.test.ts | 2 +- .../__tests__/test_root/resolve_and_require_outside.js | 4 +++- packages/jest-runtime/src/index.ts | 10 ++++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/jest-runtime/src/__tests__/runtime_require_resolve.test.ts b/packages/jest-runtime/src/__tests__/runtime_require_resolve.test.ts index ce8a48ee7c12..c46c92dba4a7 100644 --- a/packages/jest-runtime/src/__tests__/runtime_require_resolve.test.ts +++ b/packages/jest-runtime/src/__tests__/runtime_require_resolve.test.ts @@ -43,7 +43,7 @@ describe('Runtime require.resolve', () => { ); }); - describe('with the outsideJestVm option', () => { + describe('with the OUTSIDE_JEST_VM_RESOLVE_OPTION', () => { it('forwards to the real Node require in an internal context', async () => { const runtime = await createRuntime(__filename); const module = runtime.requireInternalModule( diff --git a/packages/jest-runtime/src/__tests__/test_root/resolve_and_require_outside.js b/packages/jest-runtime/src/__tests__/test_root/resolve_and_require_outside.js index 97b015d82312..52aa44551e02 100644 --- a/packages/jest-runtime/src/__tests__/test_root/resolve_and_require_outside.js +++ b/packages/jest-runtime/src/__tests__/test_root/resolve_and_require_outside.js @@ -7,7 +7,9 @@ 'use strict'; -const path = require.resolve('./create_require_module', {outsideJestVm: true}); +const path = require.resolve('./create_require_module', { + [Symbol.for('OUTSIDE_JEST_VM_RESOLVE_OPTION')]: true, +}); if (typeof path !== 'string') { throw new Error('require.resolve not spec-compliant: must return a string'); } diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index b2cefbfeb2ec..1215f2538091 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -86,8 +86,11 @@ type InitialModule = Partial & Pick; type ModuleRegistry = Map; +const OUTSIDE_JEST_VM_RESOLVE_OPTION = Symbol.for( + 'OUTSIDE_JEST_VM_RESOLVE_OPTION', +); type ResolveOptions = Parameters[1] & { - outsideJestVm?: true; + [OUTSIDE_JEST_VM_RESOLVE_OPTION]?: true; }; type BooleanObject = Record; @@ -1289,7 +1292,10 @@ class Runtime { moduleName, resolveOptions, ); - if (resolveOptions?.outsideJestVm && options?.isInternalModule) { + if ( + resolveOptions?.[OUTSIDE_JEST_VM_RESOLVE_OPTION] && + options?.isInternalModule + ) { return createOutsideJestVmPath(resolved); } return resolved; From c357ceabf67a8792f0075f524d07786bee49a26c Mon Sep 17 00:00:00 2001 From: Tim Seckinger Date: Tue, 28 Apr 2020 20:23:25 +0200 Subject: [PATCH 6/7] add test guarding against requiring constructed jest-main:// paths --- .../__tests__/runtime_require_resolve.test.ts | 26 ++++++++++++++++--- .../test_root/resolve_and_require_outside.js | 9 ++++--- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/jest-runtime/src/__tests__/runtime_require_resolve.test.ts b/packages/jest-runtime/src/__tests__/runtime_require_resolve.test.ts index c46c92dba4a7..d7ce25458f44 100644 --- a/packages/jest-runtime/src/__tests__/runtime_require_resolve.test.ts +++ b/packages/jest-runtime/src/__tests__/runtime_require_resolve.test.ts @@ -8,6 +8,7 @@ import type {Config} from '@jest/types'; import type Runtime from '..'; +import {createOutsideJestVmPath} from '../helpers'; let createRuntime: ( path: string, @@ -46,11 +47,13 @@ describe('Runtime require.resolve', () => { describe('with the OUTSIDE_JEST_VM_RESOLVE_OPTION', () => { it('forwards to the real Node require in an internal context', async () => { const runtime = await createRuntime(__filename); - const module = runtime.requireInternalModule( + const module = runtime.requireInternalModule( runtime.__mockRootPath, './resolve_and_require_outside.js', ); - expect(module).toBe(require('./test_root/create_require_module')); + expect(module.required).toBe( + require('./test_root/create_require_module'), + ); }); it('ignores the option in an external context', async () => { @@ -59,8 +62,23 @@ describe('Runtime require.resolve', () => { runtime.__mockRootPath, './resolve_and_require_outside.js', ); - expect(module.foo).toBe('foo'); - expect(module).not.toBe(require('./test_root/create_require_module')); + expect(module.required.foo).toBe('foo'); + expect(module.required).not.toBe( + require('./test_root/create_require_module'), + ); + }); + + // make sure we also check isInternal during require, not just during resolve + it('does not understand a self-constructed outsideJestVmPath in an external context', async () => { + const runtime = await createRuntime(__filename); + expect(() => + runtime.requireModule( + runtime.__mockRootPath, + createOutsideJestVmPath( + require.resolve('./test_root/create_require_module.js'), + ), + ), + ).toThrow(/cannot find.+create_require_module/i); }); }); }); diff --git a/packages/jest-runtime/src/__tests__/test_root/resolve_and_require_outside.js b/packages/jest-runtime/src/__tests__/test_root/resolve_and_require_outside.js index 52aa44551e02..e39ba2527380 100644 --- a/packages/jest-runtime/src/__tests__/test_root/resolve_and_require_outside.js +++ b/packages/jest-runtime/src/__tests__/test_root/resolve_and_require_outside.js @@ -7,10 +7,13 @@ 'use strict'; -const path = require.resolve('./create_require_module', { +const resolved = require.resolve('./create_require_module', { [Symbol.for('OUTSIDE_JEST_VM_RESOLVE_OPTION')]: true, }); -if (typeof path !== 'string') { +if (typeof resolved !== 'string') { throw new Error('require.resolve not spec-compliant: must return a string'); } -module.exports = require(path); +module.exports = { + required: require(resolved), + resolved, +}; From 6c4b1881859d41160c651b6ab5c84fd71d400be6 Mon Sep 17 00:00:00 2001 From: Tim Seckinger Date: Mon, 25 May 2020 23:39:29 +0200 Subject: [PATCH 7/7] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e061d9be64c7..57bbc7d8a17a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - `[docs]` Correct confusing filename in `enableAutomock` example ([#10055](https://github.com/facebook/jest/pull/10055)) - `[jest-core]` 🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉 ([#10000](https://github.com/facebook/jest/pull/10000)) - `[jest-core, jest-reporters, jest-test-result, jest-types]` Cleanup `displayName` type ([#10049](https://github.com/facebook/jest/pull/10049)) +- `[jest-runtime]` Jest-internal sandbox escape hatch ([#9907](https://github.com/facebook/jest/pull/9907)) ### Performance