Skip to content

Commit

Permalink
Merge pull request #18 from enduire/add-plugins
Browse files Browse the repository at this point in the history
V2
  • Loading branch information
trotzig authored Jul 4, 2018
2 parents dc1acea + 0fac710 commit 133701f
Show file tree
Hide file tree
Showing 14 changed files with 733 additions and 287 deletions.
37 changes: 37 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Contributing guide

## Testing

We use Jest as our test runner. Invoke `yarn test` to run all tests.

## Writing a plugin

A Happo plugin is basically an object with a few methods/properties defined,
along with an optional set of files.

### `customizeWebpackConfig`
Similar to the `customizeWebpackConfig` configuration option, this is a method
that allows you to extend/customize the default webpack configuration used by
Happo.

```js
module.exports = {
customizeWebpackConfig: config => {
config.plugins.push(new MyOwnWebpackPlugin()),
},
}
```

### `pathToExamplesFile`
Specify a path to a file that gets added to the list of happo example files
being parsed. The most common usecase here would be to auto-generate Happo
examples from a different source.

```js
const path = require('path');

module.exports = {
pathToExamplesFile: path.resolve(__dirname, 'myHappoExamples.js'),
}
```

83 changes: 50 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,24 @@ to ensure consistent cross-browser and responsive styling of your application.
npm install --save-dev happo.io
```

Happo depends on `babel-core` and `babel-loader` as well. If you don't already
have them installed, you need to add them as well.
Happo depends on `webpack`, `babel-core` and `babel-loader` as well. If you
don't already have them installed, you need to add them.

```
npm install --save-dev babel-core babel-loader
npm install --save-dev webpack babel-core babel-loader
```

## Getting started

Before you can run happo, you need to define one or more component example
files. We'll use React here, which is the default `type` that this client
library supports. Let's assume there's a `<Button>` component that we're adding
files. If you already have an existing source of component examples (e.g. an
existing [storybook](https://storybook.js.org/) integration, a
style-guide/component gallery), you can either use a [plugin](#plugins) or
follow the instructions in the [Generated examples](#generated-examples)
section. If not, continue reading!

We'll use React here, which is the default `type` that this client library
supports. Let's assume there's a `<Button>` component that we're adding
examples for. First, create a file called `Button-happo.jsx` and save it next
to your `Button.jsx` file (if this doesn't match your naming scheme you can use
the [`include`](#include) option). Add a few exports to this file (yes, you can
Expand Down Expand Up @@ -310,7 +316,7 @@ file is named `Button-happo.jsx`, the inferred name will be `Button`.
If you want to group multiple components in one file you can export an array
instead, with objects defining the component and its variants. This can be
handy if you for some reason want to auto-generate happo examples from another
source (e.g. [Storybook](https://storybook.js.org/), a style-guide, etc).
source (e.g. a style-guide, a component gallery etc).

```jsx
export default [
Expand All @@ -331,33 +337,6 @@ export default [
]
```

### Storybook integration

Using the example with generated examples above, you can also integrate
directly with [Storybook](https://storybook.js.org/). Here's an example using
[@storybook/react](https://storybook.js.org/basics/guide-react/):

```js
// storybook-happo.js
const { getStorybook } = require('@storybook/react');
// Import the storybook config file which will populate all the stories.
require('../.storybook/config.js');
const examples = getStorybook().map((story) => {
const variants = {};
story.stories.forEach(({ name, render }) => variants[name] = render);
return {
component: story.kind,
variants,
};
});
module.exports = examples;
```

Save this file as `storybook-happo.js` to have Happo automatically find it.

### Asynchronous examples

If you have examples that won't look right on the initial render, you can
Expand Down Expand Up @@ -404,6 +383,29 @@ Be careful about overusing async rendering as it has a tendency to lead to a
more complicated setup. In many cases it's better to factor out a "view
component" which you render synchronously in the Happo test.

## Plugins

### Storybook

The Happo plugin for [Storybook](https://storybook.js.org/) will automatically
turn your stories into Happo examples.

```bash
npm install --save-dev happo-plugin-storybook
```

```js
const happoPluginStorybook = require('happo-plugin-storybook');
// .happo.js
module.exports {
// ...
plugins: [
happoPluginStorybook(),
],
};
```

## Local development

The `happo dev` command is designed to help local development of components. In
Expand Down Expand Up @@ -596,6 +598,21 @@ module.exports = {
}
```

### `plugins`

An array of happo plugins. Find available plugins in the [Plugins](#plugins)
section.

```js
const happoPluginStorybook = require('happo-plugin-storybook');
module.exports = {
plugins: [
happoPluginStorybook(),
],
}
```

### `publicFolders`

An array of (absolute) paths specifying the places where public assets are
Expand Down
13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "happo.io",
"version": "1.5.1",
"version": "2.0.0",
"description": "Visual diffing for UI components",
"main": "./build/index.js",
"bin": {
Expand Down Expand Up @@ -33,6 +33,8 @@
},
"homepage": "https://github.com/enduire/happo.io#readme",
"jest": {
"testEnvironment": "node",
"setupTestFrameworkScriptFile": "./test/jestSetup.js",
"testMatch": [
"**/*-test.js*"
],
Expand All @@ -56,8 +58,7 @@
"request": "^2.85.0",
"request-promise-native": "^1.0.5",
"require-relative": "^0.8.7",
"supports-color": "^5.4.0",
"webpack": "^3.5.5"
"supports-color": "^5.4.0"
},
"devDependencies": {
"babel-cli": "^6.26.0",
Expand All @@ -74,10 +75,12 @@
"jest": "^22.4.3",
"react": "^16.3.2",
"react-dom": "^16.3.2",
"rimraf": "^2.6.2"
"rimraf": "^2.6.2",
"webpack": "^4.14.0"
},
"peerDependencies": {
"babel-core": "^6.0.0 || ^7.0.0",
"babel-loader": "^7.0.0 || ^8.0.0"
"babel-loader": "^7.0.0 || ^8.0.0",
"webpack": "^3.5.5 || ^4.0.0"
}
}
1 change: 1 addition & 0 deletions src/DEFAULTS.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const stylesheets = [];
export const targets = {};
export const configFile = './.happo.js';
export const type = 'react';
export const plugins = [];
export function customizeWebpackConfig(config) {
// provide a default no-op for this config option so that we can assume it's
// always there.
Expand Down
91 changes: 54 additions & 37 deletions src/createDynamicEntryPoint.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,43 +6,60 @@ import requireRelative from 'require-relative';

import findTestFiles from './findTestFiles';

export default function createDynamicEntryPoint({ setupScript, include, only, type }) {
return findTestFiles(include).then((files) => {
const filePartOfOnly = only ? only.split('#')[0] : undefined;
const fileStrings = files
.filter((f) => (filePartOfOnly ? f.includes(filePartOfOnly) : true))
.map((file) => `window.snaps['${file}'] = require('${path.join(process.cwd(), file)}');`);
const strings = [
setupScript ? `require('${setupScript}');` : '',
'window.snaps = {};',
`window.happoFlags = { only: ${JSON.stringify(only)} }`,
].concat(fileStrings);
if (type === 'react') {
const pathToReactDom = requireRelative.resolve('react-dom', process.cwd());
strings.push(
`
const ReactDOM = require('${pathToReactDom}');
window.happoRender = (component, { rootElement }) =>
ReactDOM.render(component, rootElement);
`.trim(),
);
} else {
strings.push(
`
window.happoRender = (html, { rootElement }) => {
rootElement.innerHTML = html;
return rootElement;
};
`.trim(),
);
}
strings.push('window.onBundleReady();');
const entryFile = path.join(
os.tmpdir(),
`happo-entry-${type}-${Buffer.from(process.cwd()).toString('base64')}.js`,
export default async function createDynamicEntryPoint({
setupScript,
include,
only,
type,
plugins,
}) {
const files = await findTestFiles(include);
const filePartOfOnly = only ? only.split('#')[0] : undefined;
const fileStrings = files
.filter((f) => (filePartOfOnly ? f.includes(filePartOfOnly) : true))
.map((file) => path.join(process.cwd(), file))
.concat(plugins.map(({ pathToExamplesFile }) => pathToExamplesFile))
.filter(Boolean)
.map((file) => `window.snaps['${file}'] = require('${file}');`);

const strings = [
setupScript ? `require('${setupScript}');` : '',
'window.snaps = {};',
`window.happoFlags = { only: ${JSON.stringify(only)} }`,
].concat(fileStrings);
if (type === 'react') {
const pathToReactDom = requireRelative.resolve('react-dom', process.cwd());
strings.push(
`
const ReactDOM = require('${pathToReactDom}');
window.happoRender = (component, { rootElement }) =>
ReactDOM.render(component, rootElement);
window.happoCleanup = () => {
for (const element of document.body.children) {
ReactDOM.unmountComponentAtNode(element);
}
};
`.trim(),
);
} else {
strings.push(
`
window.happoRender = (html, { rootElement }) => {
rootElement.innerHTML = html;
return rootElement;
};
window.happoCleanup = () => {};
`.trim(),
);
}
strings.push('window.onBundleReady();');
const entryFile = path.join(
os.tmpdir(),
`happo-entry-${type}-${Buffer.from(process.cwd()).toString('base64')}.js`,
);

fs.writeFileSync(entryFile, strings.join('\n'));
return { entryFile, numberOfFilesProcessed: fileStrings.length };
});
fs.writeFileSync(entryFile, strings.join('\n'));
return { entryFile, numberOfFilesProcessed: fileStrings.length };
}
11 changes: 9 additions & 2 deletions src/createWebpackBundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function generateBaseConfig(entry, type) {
},
],
},
plugins: [],
};
if (type === 'react') {
const babelPresetReact = require.resolve('babel-preset-react');
Expand All @@ -41,10 +42,16 @@ function generateBaseConfig(entry, type) {

export default function createWebpackBundle(
entry,
{ type, customizeWebpackConfig },
{ type, customizeWebpackConfig, plugins },
{ onBuildReady },
) {
const config = customizeWebpackConfig(generateBaseConfig(entry, type));
let config = generateBaseConfig(entry, type);
plugins.forEach((plugin) => {
if (typeof plugin.customizeWebpackConfig === 'function') {
config = plugin.customizeWebpackConfig(config);
}
});
config = customizeWebpackConfig(config);
const compiler = webpack(config);
const bundleFilePath = path.join(config.output.path, config.output.filename);

Expand Down
17 changes: 14 additions & 3 deletions src/domRunner.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export default async function domRunner(
publicFolders,
getRootElement,
type,
plugins,
},
{ only, onReady },
) {
Expand All @@ -107,7 +108,13 @@ export default async function domRunner(
logger.start('Reading files...');
let entryFile;
try {
const entryPointResult = await createDynamicEntryPoint({ setupScript, include, only, type });
const entryPointResult = await createDynamicEntryPoint({
setupScript,
include,
only,
type,
plugins,
});
entryFile = entryPointResult.entryFile;
logger.success(`${entryPointResult.numberOfFilesProcessed} found`);
} catch (e) {
Expand All @@ -124,7 +131,7 @@ export default async function domRunner(
// We're in dev/watch mode
createWebpackBundle(
entryFile,
{ type, customizeWebpackConfig },
{ type, customizeWebpackConfig, plugins },
{
onBuildReady: async (bundleFile) => {
if (currentBuildPromise) {
Expand Down Expand Up @@ -165,7 +172,11 @@ export default async function domRunner(
return;
}

const bundleFile = await createWebpackBundle(entryFile, { type, customizeWebpackConfig }, {});
const bundleFile = await createWebpackBundle(
entryFile,
{ type, customizeWebpackConfig, plugins },
{},
);
logger.success();
return boundGenerateScreenshots(bundleFile, logger);
}
1 change: 1 addition & 0 deletions src/processSnapsInBundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ async function processVariants({
const doc = dom.window.document;
const root = (getRootElement && getRootElement(doc)) || findRoot(doc);
const html = root.innerHTML.trim();
dom.window.happoCleanup();
return {
html,
css: '', // Can we remove this?
Expand Down
4 changes: 4 additions & 0 deletions test/integrations/examples/Button.ffs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import React from 'react';

// This is a funky file testing the customizeWebpackFunction for plugins
export default () => <button>Click me</button>;
Loading

0 comments on commit 133701f

Please sign in to comment.