Skip to content

Commit 187d155

Browse files
committed
feat(testing-karma): use native es modules in modern browsers
BREAKING CHANGE: You need to specify type: 'module' for you files ```js // old karma.conf.js files: [ config.grep ? config.grep : 'test/**/*.test.js', ] // new karma.conf.js files: [ { pattern: config.grep ? config.grep : 'test/**/*.test.js', type: 'module' }, ]
1 parent 70dff44 commit 187d155

22 files changed

+2032
-1213
lines changed

packages/karma-esm/README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# karma-esm
2+
3+
[//]: # (AUTO INSERT HEADER PREPUBLISH)
4+
5+
Karma plugin for running tests in es modules.
6+
7+
## Usage
8+
See `testing-karma` for an implementation and default configuration.
9+
10+
1. Install the plugin
11+
`npm i --save @open-wc/karma-esm`
12+
13+
2. Add to your karma config
14+
```javascript
15+
{
16+
// define where your test files are, make sure to set type to module
17+
files: [
18+
{ pattern: 'test/**/*.test.js' type: 'module' }
19+
]
20+
21+
plugins: [
22+
// load plugin
23+
require.resolve('@open-wc/karma-esm'),
24+
25+
// fallback: resolve any karma- plugins
26+
'karma-*',
27+
],
28+
29+
frameworks: ['esm'],
30+
31+
middleware: ['esm'],
32+
33+
preprocessors: {
34+
'**/*.test.js': ['esm'],
35+
'**/*.spec.js': ['esm'],
36+
},
37+
38+
esm: {
39+
// whether you want to profile for test coverage
40+
coverage: true / false,
41+
babel: {
42+
// exclude libraries which don't need babel processing for speed
43+
exclude: [
44+
'**/node_modules/sinon/**',
45+
'**/node_modules/@bundled-es-modules/**',
46+
],
47+
},
48+
},
49+
}
50+
```
51+
52+
<script>
53+
export default {
54+
mounted() {
55+
const editLink = document.querySelector('.edit-link a');
56+
if (editLink) {
57+
const url = editLink.href;
58+
editLink.href = url.substr(0, url.indexOf('/master/')) + '/master/packages/karma-esm/README.md';
59+
}
60+
}
61+
}
62+
</script>

packages/karma-esm/karma-esm.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
const merge = require('webpack-merge');
2+
const createCompiler = require('./src/compiler');
3+
4+
const createBabelOptions = (config = {}) => ({
5+
sourceType: 'module',
6+
plugins: [
7+
require.resolve('@babel/plugin-syntax-dynamic-import'),
8+
require.resolve('@babel/plugin-syntax-import-meta'),
9+
[
10+
require.resolve('../forked-babel-plugin-bare-import-rewrite'),
11+
{
12+
rootBaseDir: '.',
13+
alwaysRootImport: ['**'],
14+
modulesDir: './node_modules',
15+
failOnUnresolved: true,
16+
resolveDirectories: config.moduleDirectories,
17+
},
18+
],
19+
config.coverage && [
20+
require.resolve('babel-plugin-istanbul'),
21+
{
22+
exclude: ['**/node_modules/**', '**/*.test.js', '**/*.spec.js'],
23+
},
24+
],
25+
].filter(_ => _),
26+
sourceMap: 'inline',
27+
});
28+
29+
const defaultPluginConfig = {
30+
baseDir: './',
31+
moduleResolveRoot: './',
32+
moduleDirectories: ['node_modules'],
33+
};
34+
35+
function createPluginConfig(karmaConfig) {
36+
return merge(
37+
{
38+
babelOptions: createBabelOptions(karmaConfig.esm),
39+
},
40+
defaultPluginConfig,
41+
karmaConfig.esm,
42+
);
43+
}
44+
45+
function initialize(karmaConfig, karmaEmitter) {
46+
module.exports.pluginConfig = createPluginConfig(karmaConfig);
47+
module.exports.compile = createCompiler(module.exports.pluginConfig, karmaEmitter);
48+
}
49+
50+
module.exports = {
51+
initialize,
52+
};

packages/karma-esm/middleware.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const esm = require('./src/karma-esm');
4+
5+
function karmaEsmMiddleware(logger) {
6+
const log = logger.create('middleware.esm');
7+
8+
return (req, res, next) => {
9+
if (req.url.endsWith('.js') || req.url.endsWith('.ts')) {
10+
try {
11+
let relativeFilePath;
12+
if (req.url.startsWith('/base')) {
13+
relativeFilePath = req.url.replace('/base/', esm.pluginConfig.baseDir);
14+
}
15+
16+
esm.pluginConfig.moduleDirectories.forEach(moduleDir => {
17+
if (req.url.startsWith(`/${moduleDir}`)) {
18+
relativeFilePath = req.url.replace(
19+
`/${moduleDir}`,
20+
path.join(esm.pluginConfig.moduleResolveRoot, moduleDir),
21+
);
22+
}
23+
});
24+
25+
const fullFilePath = path.join(process.cwd(), relativeFilePath);
26+
if (!fs.existsSync(fullFilePath)) {
27+
throw new Error(`File does not exist: ${fullFilePath}`);
28+
}
29+
30+
const fileContent = fs.readFileSync(fullFilePath, 'utf-8');
31+
res.setHeader('Content-Type', 'application/javascript');
32+
try {
33+
const compiledContent = esm.compile(fullFilePath, fileContent);
34+
res.end(compiledContent);
35+
} catch (error) {
36+
const message = `\n\n${error.message}\n at ${relativeFilePath}\n\n`;
37+
log.error(message);
38+
res.end(fileContent);
39+
}
40+
} catch (error) {
41+
log.error(error.message);
42+
res.statusCode = 500;
43+
res.end(error.message);
44+
}
45+
} else {
46+
next();
47+
}
48+
};
49+
}
50+
51+
karmaEsmMiddleware.$inject = ['logger'];
52+
53+
module.exports = karmaEsmMiddleware;

packages/karma-esm/package.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "@open-wc/karma-esm",
3+
"version": "0.1.0",
4+
"description": "Karma plugin for testing with es modules",
5+
"author": "open-wc",
6+
"main": "./src/index.js",
7+
"homepage": "https://github.com/open-wc/open-wc/",
8+
"license": "MIT",
9+
"publishConfig": {
10+
"access": "public"
11+
},
12+
"repository": {
13+
"type": "git",
14+
"url": "https://github.com/open-wc/open-wc.git",
15+
"directory": "packages/karma-esm"
16+
},
17+
"scripts": {
18+
"prepublishOnly": "../../scripts/insert-header.js"
19+
},
20+
"files": [
21+
"src",
22+
"*.js"
23+
],
24+
"dependencies": {
25+
"@babel/core": "^7.3.3",
26+
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
27+
"@babel/plugin-syntax-import-meta": "^7.2.0",
28+
"@babel/polyfill": "^7.0.0",
29+
"@babel/preset-env": "^7.0.0",
30+
"@types/node": "^11.13.0",
31+
"arrify": "^2.0.1",
32+
"babel-plugin-istanbul": "^5.1.1",
33+
"chokidar": "^2.1.5",
34+
"minimatch": "^3.0.4",
35+
"path-is-inside": "^1.0.2",
36+
"resolve": "^1.10.0",
37+
"webpack-merge": "^4.2.1",
38+
"whatwg-url": "^7.0.0"
39+
}
40+
}

packages/karma-esm/preprocessor.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const esm = require('./src/karma-esm');
2+
3+
function karmaEsmPreprocessor(logger) {
4+
const log = logger.create('preprocessor.esm');
5+
6+
function preprocess(content, file, done) {
7+
try {
8+
const processed = esm.compile(file.originalPath, content);
9+
done(null, processed);
10+
} catch (e) {
11+
const message = `\n\n${e.message}\n at ${file.originalPath}\n\n`;
12+
log.error(message);
13+
done(null, content);
14+
}
15+
}
16+
17+
return preprocess;
18+
}
19+
20+
karmaEsmPreprocessor.$inject = ['logger'];
21+
22+
module.exports = karmaEsmPreprocessor;

packages/karma-esm/src/compiler.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
const babel = require('@babel/core');
2+
const minimatch = require('minimatch');
3+
const chokidar = require('chokidar');
4+
5+
/**
6+
* Sets up karma-esm compiler. Compiles files with babel, maintains
7+
* a cache and notifies karma when files change.
8+
*/
9+
function createCompiler(config, karmaEmitter) {
10+
const cache = new Map();
11+
const watcher = chokidar.watch([]);
12+
13+
watcher.on('change', filePath => {
14+
if (!filePath.endsWith('.test.js') && !filePath.endsWith('.spec.js')) {
15+
karmaEmitter.refreshFiles();
16+
}
17+
cache.delete(filePath);
18+
});
19+
20+
function addToCache(filePath, code) {
21+
cache.set(filePath, code);
22+
watcher.add(filePath);
23+
}
24+
25+
function babelCompile(filePath, code) {
26+
return babel.transform(code, {
27+
filename: filePath,
28+
...config.babelOptions,
29+
}).code;
30+
}
31+
32+
function compile(filePath, code) {
33+
// if we should not babel compile some files, only add them to the cache
34+
if (config.babel && config.babel.exclude) {
35+
if (config.babel.exclude.some(exclude => minimatch(filePath, exclude))) {
36+
addToCache(filePath, code);
37+
return code;
38+
}
39+
}
40+
41+
const compiled = babelCompile(filePath, code);
42+
addToCache(filePath, compiled);
43+
return compiled;
44+
}
45+
46+
function getCached(filePath) {
47+
return cache.get(filePath);
48+
}
49+
50+
return {
51+
compile,
52+
getCached,
53+
};
54+
}
55+
56+
module.exports = createCompiler;

0 commit comments

Comments
 (0)