-
-
Notifications
You must be signed in to change notification settings - Fork 298
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
397 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@web/test-runner-intercept': patch | ||
--- | ||
|
||
initial commit of @web/test-runner-intercept |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
## Usage | ||
|
||
Setup | ||
|
||
```js | ||
// web-test-runner.config.mjs | ||
import { interceptModulePlugin } from '@web/test-runner-intercept/plugin'; | ||
|
||
export default { | ||
plugins: [interceptModulePlugin()], | ||
}; | ||
``` | ||
|
||
```js | ||
// src/getTimeOfDay.js | ||
import { getCurrentHour } from 'time-library'; | ||
|
||
export function getTimeOfDay() { | ||
const hour = getCurrentHour(); | ||
if (hour < 6 || hour > 18) { | ||
return 'night'; | ||
} | ||
return 'day'; | ||
} | ||
``` | ||
|
||
Simple test scenario: | ||
|
||
```js | ||
// test/getTimeOfDay.test.js | ||
import { interceptModule } from '@web/test-runner-intercept'; | ||
|
||
const timeLibrary = await interceptModule('time-library'); | ||
const { getTimeOfDay } = await import('../src/getTimeOfDay.js'); | ||
|
||
describe('getTimeOfDay', () => { | ||
it('returns night at 2', () => { | ||
timeLibrary.getCurrentHour = () => 2; | ||
const result = getTimeOfDay(); | ||
if (result !== 'night') { | ||
throw; | ||
} | ||
}); | ||
}); | ||
``` | ||
|
||
More extended test scenario with common helper libraries: | ||
|
||
```js | ||
// test/getTimeOfDay.test.js | ||
import { stub } from 'sinon'; | ||
import { expect } from 'chai'; | ||
import { interceptModule } from '@web/test-runner-intercept'; | ||
|
||
const timeLibrary = await interceptModule('time-library'); | ||
const { getTimeOfDay } = await import('../src/getTimeOfDay.js'); | ||
|
||
describe('getTimeOfDay', () => { | ||
it('returns night at 2', () => { | ||
const stubGetCurrentHour = stub(timeLibrary, 'getCurrentHour').returns(2); | ||
try { | ||
const result = getTimeOfDay(); | ||
expect(result).to.equal('night'); | ||
} finally { | ||
stubGetCurrentHour.restore(); | ||
} | ||
}); | ||
it('returns day at 14', () => { | ||
const stubGetCurrentHour = stub(timeLibrary, 'getCurrentHour').returns(14); | ||
try { | ||
const result = getTimeOfDay(); | ||
expect(result).to.equal('day'); | ||
} finally { | ||
stubGetCurrentHour.restore(); | ||
} | ||
}); | ||
}); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
/** | ||
* Intercept a module in order to change its implementation. | ||
* @param moduleName The name of the module to intercept. When intercepting the import of a module, the name should exactly match the name of the import. | ||
* @returns Writable object with every named export of the intercepted module as a property. | ||
* If the module to be intercepted contains a default export, the export will be available in the `default` property. Only function expressions are supported when intercepting default exports. | ||
*/ | ||
export function interceptModule(moduleName: string): Promise<Record<string, func> | void>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
/** | ||
* Intercept a module in order to change its implementation. | ||
* @param {string} moduleName The name of the module to intercept. When intercepting the import of a module, the name should exactly match the name of the import. | ||
* @returns {Promise<Record<string, func> | void >} Writable object with every named export of the intercepted module as a property. | ||
* If the module to be intercepted contains a default export, the export will be available in the `default` property. Only function expressions are supported when intercepting default exports. | ||
*/ | ||
export async function interceptModule(moduleName) { | ||
let module; | ||
try { | ||
module = await import(`/__intercept-module__/${moduleName}`); | ||
} catch (e) { | ||
throw new Error( | ||
`Module interception is not active. Make sure the \`interceptModulePlugin\` of \`@web/test-runner-intercept\` is added to the Test Runner config.`, | ||
); | ||
} | ||
if (module.__wtr_error__) { | ||
throw new Error(module.__wtr_error__); | ||
} | ||
return module.__wtr_intercepted_module__; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
// this file is autogenerated with the generate-mjs-dts-entrypoints script | ||
export * from './browser/index'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
{ | ||
"name": "@web/test-runner-intercept", | ||
"version": "0.1.0", | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"description": "Plugin for intercepting modules in @web/test-runner", | ||
"license": "MIT", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/modernweb-dev/web.git", | ||
"directory": "packages/test-runner-intercept" | ||
}, | ||
"author": "modern-web", | ||
"homepage": "https://github.com/modernweb-dev/web/tree/master/packages/test-runner-intercept", | ||
"main": "browser/index.js", | ||
"exports": { | ||
".": { | ||
"types": "./browser/index.d.ts", | ||
"default": "./browser/index.js" | ||
}, | ||
"./plugin": { | ||
"types": "./plugin.d.ts", | ||
"import": "./plugin.mjs", | ||
"require": "./dist/interceptModulePlugin.js" | ||
} | ||
}, | ||
"engines": { | ||
"node": ">=16.0.0" | ||
}, | ||
"scripts": { | ||
"build": "tsc", | ||
"test": "mocha \"test/**/*.test.ts\" --require ts-node/register && npm run test:ci", | ||
"test:browser": "node ../test-runner/dist/bin.js test-browser/test/**/*.test.{js,html} --config test-browser/web-test-runner.config.mjs", | ||
"test:ci": "npm run test:browser", | ||
"test:watch": "mocha \"test/**/*.test.ts\" --require ts-node/register --watch --watch-files src,test" | ||
}, | ||
"files": [ | ||
"*.d.ts", | ||
"*.js", | ||
"*.mjs", | ||
"dist", | ||
"src" | ||
], | ||
"keywords": [ | ||
"web", | ||
"dev", | ||
"server", | ||
"test", | ||
"runner", | ||
"testrunner", | ||
"module", | ||
"interception", | ||
"intercept" | ||
], | ||
"dependencies": { | ||
"@web/dev-server-core": "^0.6.2", | ||
"es-module-lexer": "^1.3.1" | ||
}, | ||
"devDependencies": { | ||
"@web/test-runner": "^0.17.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
// this file is autogenerated with the generate-mjs-dts-entrypoints script | ||
export * from './dist/interceptModulePlugin'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
// this file is autogenerated with the generate-mjs-dts-entrypoints script | ||
import cjsEntrypoint from './dist/index.js'; | ||
|
||
const { interceptModulePlugin } = cjsEntrypoint; | ||
|
||
export { interceptModulePlugin }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { Plugin, ServerStartParams, ResolveOptions, Context } from '@web/dev-server-core'; | ||
|
||
// Copied from packages/dev-server-core/src/plugins/Plugin.ts as it's not exported | ||
export type ResolveResult = void | string | { id?: string }; | ||
export interface ResolveImportArguments { | ||
source: string; | ||
context: Context; | ||
code?: string; | ||
column?: number; | ||
line?: number; | ||
resolveOptions?: ResolveOptions; | ||
} | ||
export type ResolveImport = ( | ||
args: ResolveImportArguments, | ||
) => ResolveResult | Promise<ResolveResult>; | ||
|
||
/** | ||
* TODO: check if `resolveImport()` can be provied by `@web/dev-server-core`'s API | ||
* @param args start param args | ||
* @param thisPlugin plugin to exclude | ||
*/ | ||
export function createResolveImport( | ||
{ config }: ServerStartParams, | ||
thisPlugin: Plugin, | ||
): ResolveImport { | ||
const resolvePlugins = | ||
config.plugins?.filter?.(pl => pl.resolveImport && pl !== thisPlugin) ?? []; | ||
|
||
return async function resolveImport(args: ResolveImportArguments) { | ||
for (const plugin of resolvePlugins) { | ||
const resolved = await plugin?.resolveImport?.(args); | ||
if (typeof resolved === 'string') { | ||
return resolved; | ||
} | ||
|
||
if (typeof resolved === 'object') { | ||
return resolved?.id; | ||
} | ||
} | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { interceptModulePlugin } from './interceptModulePlugin'; |
87 changes: 87 additions & 0 deletions
87
packages/test-runner-intercept/src/interceptModulePlugin.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import { Plugin } from '@web/dev-server-core'; | ||
import { readFile } from 'fs/promises'; | ||
import { parse } from 'es-module-lexer'; | ||
import { createResolveImport, ResolveImport } from './createResolveImport'; | ||
import { stripColor } from './stripColor'; | ||
|
||
/** | ||
* Plugin that allows the interception of modules | ||
*/ | ||
export function interceptModulePlugin(): Plugin { | ||
const paths: string[] = []; | ||
|
||
let resolveImport: ResolveImport; | ||
return { | ||
name: 'intercept-module', | ||
|
||
serverStart(params) { | ||
resolveImport = createResolveImport(params, this); | ||
}, | ||
async serve(context) { | ||
if (context.path.startsWith('/__intercept-module__/')) { | ||
let body; | ||
try { | ||
const source = context.path.replace('/__intercept-module__/', ''); | ||
paths.push(source); | ||
|
||
const resolvedPath = await resolveImport({ context, source }); | ||
const url = new URL(resolvedPath as string, context.request.href); | ||
const relativeFilePath = url.pathname.substring(1); | ||
|
||
const content = await readFile(relativeFilePath, 'utf-8'); | ||
const [, exports] = await parse(content, resolvedPath as string); | ||
|
||
const namedExports = exports.map(e => e.n).filter(n => n !== 'default'); | ||
const hasDefaultExport = exports.some(e => e.n === 'default'); | ||
|
||
body = ` | ||
import * as original from '${resolvedPath}'; | ||
const newOriginal = {...original}; | ||
${namedExports.map(x => `export let ${x} = newOriginal['${x}'];`).join('\n')} | ||
${ | ||
hasDefaultExport | ||
? ` | ||
function computeDefault() { | ||
if(typeof newOriginal.default === 'function'){ | ||
return (...args) => newOriginal.default.call(undefined, ...args); | ||
} | ||
return newOriginal.default; | ||
} | ||
export default computeDefault() | ||
` | ||
: '' | ||
} | ||
export const __wtr_intercepted_module__ = new Proxy(newOriginal, { | ||
set: function(obj, prop, value) { | ||
${namedExports.map(x => `if (prop === '${x}') { ${x} = value;}`).join('\n')} | ||
return Reflect.set(obj, prop, value); | ||
}, | ||
defineProperty(target, key, descriptor) { | ||
${namedExports.map(x => `if (key === '${x}') { ${x} = descriptor.value;}`).join('\n')} | ||
return Reflect.defineProperty(target, key, descriptor); | ||
}, | ||
}); | ||
`; | ||
} catch (error) { | ||
// Server side errors might contain ANSI color escape sequences. | ||
// These sequences are not readable in a browser's console, so we strip them. | ||
const errorMessage = stripColor((error as Error).message).replaceAll("'", "\\'"); | ||
body = `export const __wtr_error__ = '${errorMessage}';`; | ||
} | ||
return { body, type: 'text/javascript' }; | ||
} | ||
return undefined; | ||
}, | ||
|
||
resolveImport({ source }) { | ||
if (paths.includes(source)) { | ||
return `/__intercept-module__/${source}`; | ||
} | ||
return undefined; | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export function stripColor(str: string): string { | ||
// eslint-disable-next-line no-control-regex | ||
return str.replace(/\x1B[[(?);]{0,2}(;?\d)*./g, ''); | ||
} |
Oops, something went wrong.