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

feat: use shared bundles #380

Merged
merged 1 commit into from
Dec 13, 2018
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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
.eslintcache

lib
coverage
node_modules

Expand Down
123 changes: 65 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ npm i -D karma-webpack
module.exports = (config) => {
config.set({
// ... normal karma configuration

// add webpack to your list of frameworks
frameworks: ['mocha', 'webpack'],

files: [
// all files ending in "_test"
{ pattern: 'test/*_test.js', watched: false },
{ pattern: 'test/**/*_test.js', watched: false }
// each file acts as entry point for the webpack configuration
// all files ending in ".test.js"
'test/**/*.test.js',
],

preprocessors: {
Expand All @@ -46,51 +48,79 @@ module.exports = (config) => {

webpack: {
// karma watches the test entry points
// (you don't need to specify the entry option)
// Do NOT specify the entry option
// webpack watches dependencies

// webpack configuration
},

webpackMiddleware: {
// webpack-dev-middleware configuration
// i. e.
stats: 'errors-only'
}
})
});
}
```

### `Alternative Usage`

This configuration is more performant, but you cannot run single test anymore (only the complete suite).
### Default webpack configuration

The above configuration generates a `webpack` bundle for each test. For many test cases this can result in many big files. The alternative configuration creates a single bundle with all test cases.
This configuration will be merged with what gets provided via karma's config.webpack.

**karma.conf.js**
```js
files: [
// only specify one entry point
// and require all tests in there
'test/index_test.js'
],

preprocessors: {
// add webpack as preprocessor
'test/index_test.js': [ 'webpack' ]
},
const defaultWebpackOptions = {
mode: 'development',
output: {
filename: '[name].js',
path: path.join(os.tmpdir(), '_karma_webpack_'),
},
stats: {
modules: false,
colors: true,
},
watch: false,
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
minSize: 0,
cacheGroups: {
commons: {
name: 'commons',
chunks: 'initial',
minChunks: 1,
},
},
},
},
plugins: [],
// Something like this will be auto added by this.configure()
// entry: {
// 'foo-one.test.js': 'path/to/test/foo-one.test.js',
// 'foo-two.test.js': 'path/to/test/foo-two.test.js',
// },
// plugins: [
// new KarmaSyncPlugin()
// ],
};
```

**test/index_test.js**
```js
// require all modules ending in "_test" from the
// current directory and all subdirectories
const testsContext = require.context(".", true, /_test$/)
### How it works

testsContext.keys().forEach(testsContext)
```
This project is a framework and preprocessor for Karma that combines test files and dependencies into 2 shared bundles and 1 chunk per test file. It relies on webpack to generate the bundles/chunks and to keep it updated during autoWatch=true.

The first preproccessor triggers the build of all the bundles/chunks and all following files just return the output of this one build process.

### Webpack typescript support

By default karma-webpack forces *.js files so if you test *.ts files and use webpack to build typescript to javascript it works out of the box.

Every test file is required using the [require.context](https://webpack.js.org/guides/dependency-management/#require-context) and compiled with webpack into one test bundle.
If you have a different need you can override by settig `webpack.transformPath`

```js
// this is the by default applied transformPath
webpack: {
transformPath: (filepath) => {
// force *.js files by default
const info = path.parse(filepath);
return `${path.join(info.dir, info.name)}.js`;
},
},
```

### `Source Maps`

Expand Down Expand Up @@ -126,34 +156,11 @@ This is the full list of options you can specify in your `karma.conf.js`
|Name|Type|Default|Description|
|:--:|:--:|:-----:|:----------|
|[**`webpack`**](#webpack)|`{Object}`|`{}`|Pass `webpack.config.js` to `karma`|
|[**`webpackMiddleware`**](#webpackmiddleware)|`{Object}`|`{}`|Pass `webpack-dev-middleware` configuration to `karma`|
|[**`beforeMiddleware`**](#beforemiddleware)|`{Object}`|`{}`|Pass custom middleware configuration to `karma`, **before** any `karma` middleware runs|

### `webpack`

`webpack` configuration (`webpack.config.js`).

### `webpackMiddleware`

Configuration for `webpack-dev-middleware`.

### `beforeMiddleware`

`beforeMiddleware` is a `webpack` option that allows injecting middleware before
karma's own middleware runs. This loader provides a `webpackBlocker`
middleware that will block tests from running until code recompiles. That is,
given this scenario

1. Have a browser open on the karma debug page (http://localhost:9876/debug.html)
2. Make a code change
3. Refresh

Without the `webpackBlocker` middleware karma will serve files from before
the code change. With the `webpackBlocker` middleware the loader will not serve
the files until the code has finished recompiling.

> **⚠️ The `beforeMiddleware` option is only supported in `karma >= v1.0.0`**

<h2 align="center">Maintainers</h2>

<table>
Expand Down
181 changes: 181 additions & 0 deletions lib/KarmaWebpackController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/* eslint-disable no-console */

const path = require('path');
const fs = require('fs');
const os = require('os');

const webpack = require('webpack');
const merge = require('webpack-merge');

class KarmaSyncPlugin {
constructor(options) {
this.karmaEmitter = options.karmaEmitter;
this.controller = options.controller;
}

apply(compiler) {
this.compiler = compiler;

// webpack bundles are finished
compiler.hooks.done.tap('KarmaSyncPlugin', async (stats) => {
// read generated file content and store for karma preprocessor
this.controller.bundlesContent = {};
stats.toJson().assets.forEach((webpackFileObj) => {
const filePath = `${compiler.options.output.path}/${
webpackFileObj.name
}`;
this.controller.bundlesContent[webpackFileObj.name] = fs.readFileSync(
filePath,
'utf-8'
);
});

// karma refresh
this.karmaEmitter.refreshFiles();
});
}
}

const defaultWebpackOptions = {
mode: 'development',
output: {
filename: '[name].js',
path: path.join(os.tmpdir(), '_karma_webpack_'),
},
stats: {
modules: false,
colors: true,
},
watch: false,
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
minSize: 0,
cacheGroups: {
commons: {
name: 'commons',
chunks: 'initial',
minChunks: 1,
},
},
},
},
plugins: [],
// Something like this will be auto added by this.configure()
// entry: {
// 'foo-one.test.js': 'path/to/test/foo-one.test.js',
// 'foo-two.test.js': 'path/to/test/foo-two.test.js',
// },
// plugins: [
// new KarmaSyncPlugin()
// ],
};

class KarmaWebpackController {
set webpackOptions(options) {
this.__webpackOptions = options;
}

get webpackOptions() {
return this.__webpackOptions;
}

set karmaEmitter(emitter) {
this.__karmaEmitter = emitter;

this.__webpackOptions.plugins.push(
new KarmaSyncPlugin({
karmaEmitter: emitter,
controller: this,
})
);

emitter.on('exit', (done) => {
this.onKarmaExit();
done();
});
}

get karmaEmitter() {
return this.__karmaEmitter;
}

get outputPath() {
return this.webpackOptions.output.path;
}

constructor() {
this.isActive = false;
this.bundlesContent = {};
this.__debounce = false;
this.webpackOptions = defaultWebpackOptions;
}

updateWebpackOptions(newOptions) {
this.webpackOptions = merge(this.webpackOptions, newOptions);
}

async bundle() {
if (this.isActive === false && this.__debounce === false) {
console.log('Webpack bundling...');
this._activePromise = this._bundle();
}
return this._activePromise;
}

async _bundle() {
this.isActive = true;
this.__debounce = true;
this.compiler = webpack(this.webpackOptions);
return new Promise((resolve) => {
if (this.webpackOptions.watch === true) {
console.log('Webpack starts watching...');
this.webpackFileWatcher = this.compiler.watch({}, (err, stats) =>
this.handleBuildResult(err, stats, resolve)
);
} else {
this.compiler.run((err, stats) =>
this.handleBuildResult(err, stats, resolve)
);
}
});
}

handleBuildResult(err, stats, resolve) {
if (err) {
console.error(err.stack || err);
if (err.details) {
console.error(err.details);
}
return;
}

const info = stats.toJson();
if (stats.hasErrors()) {
console.error(info.errors);
}
if (stats.hasWarnings()) {
console.warn(info.warnings);
}

this.__debounce = setTimeout(() => (this.__debounce = false), 100);
this.isActive = false;

console.log(stats.toString(this.webpackOptions.stats));
resolve();
}

onKarmaExit() {
if (this.webpackFileWatcher) {
this.webpackFileWatcher.close();
console.log('Webpack stopped watching.');
}
}
}

module.exports = {
KarmaSyncPlugin,
KarmaWebpackController,
defaultWebpackOptions,
};
File renamed without changes.
Loading