[7.x] [canvas][storybook] Improve Storybook Performance (#34757) (#35090)

* [canvas][storybook] Improve Storybook Performance (#34757)

* [WIP] Initial commit

* [canvas][storybook] Improving Storybook Performance

* Adding docs; fixing bugs

* Address feedback; add todo

* Delete stats.json
This commit is contained in:
Clint Andrew Hall 2019-04-15 13:40:56 -05:00 committed by GitHub
parent 291320ceb1
commit ee4a76fb2a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 400 additions and 142 deletions

View file

@ -17,7 +17,6 @@
* under the License.
*/
/**
* These patterns are used to identify files that are not supposed
* to be snake_case because their names are determined by other
@ -43,10 +42,12 @@ export const IGNORE_FILE_GLOBS = [
'x-pack/docs/**/*',
'src/legacy/ui/public/assets/fonts/**/*',
// filename must match language code which requires capital letters
'**/translations/*.json'
];
// Files in this directory must match a pre-determined name in some cases.
'x-pack/plugins/canvas/.storybook/*',
// filename must match language code which requires capital letters
'**/translations/*.json',
];
/**
* These patterns are matched against directories and indicate
@ -54,11 +55,7 @@ export const IGNORE_FILE_GLOBS = [
*
* @type {Array}
*/
export const KEBAB_CASE_DIRECTORY_GLOBS = [
'packages/*',
'x-pack',
];
export const KEBAB_CASE_DIRECTORY_GLOBS = ['packages/*', 'x-pack'];
/**
* These patterns are matched against directories and indicate
@ -88,7 +85,6 @@ export const IGNORE_DIRECTORY_GLOBS = [
'x-pack/dev-tools',
];
/**
* DO NOT ADD FILES TO THIS LIST!!
*

View file

@ -22,32 +22,25 @@ import { resolve } from 'path';
import { toArray } from 'rxjs/operators';
import { createFailError } from '../run';
import { findPluginSpecs } from '../../legacy/plugin_discovery';
import { collectUiExports } from '../../legacy/ui';
import { buildAll } from '../../legacy/server/sass/build_all';
import { findPluginSpecs } from '../../legacy/plugin_discovery';
import { collectUiExports } from '../../legacy/ui';
import { buildAll } from '../../legacy/server/sass/build_all';
import chokidar from 'chokidar';
import debounce from 'lodash/function/debounce';
export async function buildSass({ log, kibanaDir }) {
log.info('running plugin discovery in', kibanaDir);
const scanDirs = [
resolve(kibanaDir, 'src/legacy/core_plugins')
];
const paths = [ resolve(kibanaDir, 'x-pack') ];
const { spec$ } = findPluginSpecs({ plugins: { scanDirs, paths } });
const enabledPlugins = await spec$.pipe(toArray()).toPromise();
const uiExports = collectUiExports(enabledPlugins);
log.info('found %d styleSheetPaths', uiExports.styleSheetPaths.length);
log.verbose(uiExports.styleSheetPaths);
// TODO: clintandrewhall - Extract and use FSWatcher from legacy/server/sass
const build = async ({ log, kibanaDir, styleSheetPaths, watch }) => {
if (styleSheetPaths.length === 0) {
return;
}
let bundleCount = 0;
try {
const bundles = await buildAll({
styleSheets: uiExports.styleSheetPaths,
styleSheets: styleSheetPaths,
log,
buildDir: resolve(kibanaDir, 'built_assets/css'),
sourceMap: true
sourceMap: true,
});
bundles.forEach(bundle => {
@ -60,5 +53,41 @@ export async function buildSass({ log, kibanaDir }) {
throw createFailError(`${message} on line ${line} of ${file}`);
}
log.success('%d scss bundles created', bundleCount);
log.success('%d scss bundles %s', bundleCount, watch ? 'rebuilt' : 'created');
};
export async function buildSass({ log, kibanaDir, watch }) {
log.info('running plugin discovery in', kibanaDir);
const scanDirs = [resolve(kibanaDir, 'src/legacy/core_plugins')];
const paths = [resolve(kibanaDir, 'x-pack')];
const { spec$ } = findPluginSpecs({ plugins: { scanDirs, paths } });
const enabledPlugins = await spec$.pipe(toArray()).toPromise();
const uiExports = collectUiExports(enabledPlugins);
const { styleSheetPaths } = uiExports;
log.info('%s %d styleSheetPaths', watch ? 'watching' : 'found', styleSheetPaths.length);
log.verbose(styleSheetPaths);
if (watch) {
const debouncedBuild = debounce(async path => {
let buildPaths = styleSheetPaths;
if (path) {
buildPaths = styleSheetPaths.filter(styleSheetPath =>
path.includes(styleSheetPath.urlImports.publicDir)
);
}
await build({ log, kibanaDir, styleSheetPaths: buildPaths, watch });
});
const watchPaths = styleSheetPaths.map(styleSheetPath => styleSheetPath.urlImports.publicDir);
await build({ log, kibanaDir, styleSheetPaths });
chokidar.watch(watchPaths, { ignoreInitial: true }).on('all', (_, path) => {
debouncedBuild(path);
});
} else {
await build({ log, kibanaDir, styleSheetPaths });
}
}

View file

@ -18,23 +18,30 @@
*/
import { run } from '../run';
import { REPO_ROOT } from '../constants';
import { REPO_ROOT } from '../constants';
import { buildSass } from './build_sass';
run(async ({ log, flags: { kibanaDir } }) => {
await buildSass({
log,
kibanaDir
});
}, {
description: 'Simple CLI, useful for building scss files outside of the server',
flags: {
default: {
kibanaDir: REPO_ROOT
},
string: ['kibanaDir'],
help: `
--kibanaDir The root of the Kibana directory to build sass files in.
`
run(
async ({ log, flags: { kibanaDir, watch } }) => {
await buildSass({
log,
kibanaDir,
watch,
});
},
});
{
description: 'Simple CLI, useful for building scss files outside of the server',
flags: {
default: {
kibanaDir: REPO_ROOT,
watch: false,
},
string: ['kibanaDir'],
boolean: ['watch'],
help: `
--kibanaDir The root of the Kibana directory to build sass files in.
--watch Watch the SASS files and recompile them on save.
`,
},
}
);

View file

@ -6,6 +6,7 @@
"license": "Elastic-License",
"scripts": {
"kbn": "node ../scripts/kbn",
"kbn:bootstrap": "rm -rf ../built_assets/canvasStorybookDLL",
"start": "gulp dev",
"build": "gulp build",
"testonly": "gulp testonly",
@ -119,8 +120,7 @@
"pdfjs-dist": "^2.0.943",
"pixelmatch": "4.0.2",
"proxyquire": "1.7.11",
"react-docgen-typescript-loader": "^3.0.0",
"react-docgen-typescript-webpack-plugin": "^1.1.0",
"react-docgen-typescript-loader": "^3.1.0",
"react-hooks-testing-library": "^0.3.8",
"react-test-renderer": "^16.8.0",
"react-testing-library": "^6.0.0",

View file

@ -9,11 +9,6 @@ import { withKnobs } from '@storybook/addon-knobs/react';
import { withInfo } from '@storybook/addon-info';
import { create } from '@storybook/theming';
// Import dependent CSS
require('@elastic/eui/dist/eui_theme_light.css');
require('@kbn/ui-framework/dist/kui_light.css');
require('../../../../src/legacy/ui/public/styles/bootstrap_light.less');
// If we're running Storyshots, be sure to register the require context hook.
// Otherwise, add the other decorators.
if (process.env.NODE_ENV === 'test') {
@ -39,17 +34,16 @@ if (process.env.NODE_ENV === 'test') {
}
function loadStories() {
// Pull in the built CSS produced by the Kibana server
const css = require.context('../../../../built_assets/css', true, /light.css$/);
css.keys().forEach(filename => css(filename));
require('./dll_contexts');
// Include the legacy styles
const uiStyles = require.context(
'../../../../src/legacy/ui/public/styles',
false,
/[\/\\](?!mixins|variables|_|\.|bootstrap_(light|dark))[^\/\\]+\.less/
// Only gather and require CSS files related to Canvas. The other CSS files
// are built into the DLL.
const css = require.context(
'../../../../built_assets/css',
true,
/plugins\/(?=canvas).*light\.css/
);
uiStyles.keys().forEach(key => uiStyles(key));
css.keys().forEach(filename => css(filename));
// Find all files ending in *.examples.ts
const req = require.context('./..', true, /.examples.tsx$/);

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
const path = require('path');
const DLL_NAME = 'canvasStorybookDLL';
const KIBANA_ROOT = path.resolve(__dirname, '../../../..');
const BUILT_ASSETS = path.resolve(KIBANA_ROOT, 'built_assets');
const DLL_OUTPUT = path.resolve(BUILT_ASSETS, DLL_NAME);
module.exports = {
DLL_NAME,
KIBANA_ROOT,
BUILT_ASSETS,
DLL_OUTPUT,
};

View file

@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// This file defines CSS and Legacy style contexts for use in the DLL. This file
// is also require'd in the Storybook config so that the Storybook Webpack instance
// is aware of them, and can load them from the DLL.
// Pull in the built CSS produced by the Kibana server, but not
// the Canvas CSS-- we want that in the HMR service.
const css = require.context(
'../../../../built_assets/css',
true,
/\.\/plugins\/(?!canvas).*light\.css/
);
css.keys().forEach(filename => {
css(filename);
});
// Include Legacy styles
const uiStyles = require.context(
'../../../../src/legacy/ui/public/styles',
false,
/[\/\\](?!mixins|variables|_|\.|bootstrap_(light|dark))[^\/\\]+\.less/
);
uiStyles.keys().forEach(key => uiStyles(key));

View file

@ -7,7 +7,7 @@
const serve = require('serve-static');
const path = require('path');
// Extend the Storybook Middleware to include a route to access ui assets
module.exports = function (router) {
// Extend the Storybook Middleware to include a route to access Legacy UI assets
module.exports = function(router) {
router.get('/ui', serve(path.resolve(__dirname, '../../../../src/legacy/ui/public/assets')));
}
};

View file

@ -0,0 +1,6 @@
<!--
This file is looked for by Storybook and included in the HEAD element
if it exists. This is how we load the DLL content into the Storybook UI.
-->
<script src="/dll.js"></script>
<link href="/dll.css" rel="stylesheet" />

View file

@ -7,11 +7,13 @@
import path from 'path';
import initStoryshots, { multiSnapshotWithOptions } from '@storybook/addon-storyshots';
import styleSheetSerializer from 'jest-styled-components/src/styleSheetSerializer';
import { addSerializer } from 'jest-specific-snapshot'
import { addSerializer } from 'jest-specific-snapshot';
jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `generated-id`);
addSerializer(styleSheetSerializer);
// Initialize Storyshots and build the Jest Snapshots
initStoryshots({
configPath: path.resolve(__dirname, './../.storybook'),
test: multiSnapshotWithOptions({}),

View file

@ -5,20 +5,12 @@
*/
const path = require('path');
const TSDocgenPlugin = require('react-docgen-typescript-webpack-plugin');
// Extend the Storybook Webpack config with some customizations;
module.exports = async ({ config, _mode }) => {
// Include the React preset for Storybook JS files.
config.module.rules.push({
test: /\.js$/,
exclude: /node_modules/,
loaders: 'babel-loader',
options: {
presets: [require.resolve('@kbn/babel-preset/webpack_preset')],
},
});
const webpack = require('webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { DLL_OUTPUT, KIBANA_ROOT } = require('./constants');
// Extend the Storybook Webpack config with some customizations
module.exports = async ({ config }) => {
// Find and alter the CSS rule to replace the Kibana public path string with a path
// to the route we've added in middleware.js
const cssRule = config.module.rules.find(rule => rule.test.source.includes('.css$'));
@ -31,23 +23,17 @@ module.exports = async ({ config, _mode }) => {
},
});
// Configure loading LESS files from Kibana
// Include the React preset from Kibana for Storybook JS files.
config.module.rules.push({
test: /\.less$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader', options: { importLoaders: 2 } },
{
loader: 'postcss-loader',
options: {
config: { path: path.resolve(__dirname, './../../../../src/optimize/postcss.config.js') },
},
},
{ loader: 'less-loader' },
],
test: /\.js$/,
exclude: /node_modules/,
loaders: 'babel-loader',
options: {
presets: [require.resolve('@kbn/babel-preset/webpack_preset')],
},
});
// Support .ts/x files using the tsconfig from Kibana
// Handle Typescript files
config.module.rules.push({
test: /\.tsx?$/,
use: [
@ -57,18 +43,48 @@ module.exports = async ({ config, _mode }) => {
presets: [require.resolve('@kbn/babel-preset/webpack_preset')],
},
},
],
});
// Parse props data for .tsx files
config.module.rules.push({
test: /\.tsx$/,
// Exclude example files, as we don't display props info for them
exclude: /\.examples.tsx$/,
use: [
// Parse TS comments to create Props tables in the UI
require.resolve('react-docgen-typescript-loader'),
],
});
// Include the TSDocgen plugin to display Typescript param comments in the stories.
config.plugins.push(new TSDocgenPlugin());
// Reference the built DLL file of static(ish) dependencies, which are removed
// during kbn:bootstrap and rebuilt if missing.
config.plugins.push(
new webpack.DllReferencePlugin({
manifest: path.resolve(DLL_OUTPUT, 'manifest.json'),
context: KIBANA_ROOT,
})
);
// Copy the DLL files to the Webpack build for use in the Storybook UI
config.plugins.push(
new CopyWebpackPlugin([
{
from: path.resolve(DLL_OUTPUT, 'dll.js'),
to: 'dll.js',
},
{
from: path.resolve(DLL_OUTPUT, 'dll.css'),
to: 'dll.css',
},
])
);
// Tell Webpack about the ts/x extensions
config.resolve.extensions.push('.ts', '.tsx');
// Alias the any imports from ui/ to the proper directory.
config.resolve.alias.ui = path.resolve(__dirname, './../../../../src/legacy/ui/public');
config.resolve.alias.ui = path.resolve(KIBANA_ROOT, 'src/legacy/ui/public');
return config;
};

View file

@ -0,0 +1,115 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
const webpack = require('webpack');
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { DLL_NAME, DLL_OUTPUT, KIBANA_ROOT } = require('./constants');
// This is the Webpack config for the DLL of CSS and JS assets that are
// not expected to change during development. This saves compile and run
// times considerably.
module.exports = {
context: KIBANA_ROOT,
mode: 'development',
// This is a (potentially growing) list of modules that can be safely
// included in the DLL. Only add to this list modules or other code
// which Storybook stories and their components would require, but don't
// change during development.
entry: [
'@elastic/eui',
'@elastic/eui/dist/eui_theme_light.css',
'@kbn/ui-framework/dist/kui_light.css',
'@storybook/addon-actions',
'@storybook/addon-actions/register',
'@storybook/addon-info',
'@storybook/addon-knobs',
'@storybook/addon-knobs/react',
'@storybook/addon-knobs/register',
'@storybook/addon-options',
'@storybook/addon-options/register',
'@storybook/core',
'@storybook/core/dist/server/common/polyfills.js',
'@storybook/react',
'@storybook/theming',
'chroma-js',
'lodash',
'prop-types',
'react-dom',
'react',
'recompose',
'tinycolor2',
// Include the DLL UI contexts from Kibana
require.resolve('./dll_contexts'),
],
plugins: [
// Produce the DLL and its manifest
new webpack.DllPlugin({
name: DLL_NAME,
path: path.resolve(DLL_OUTPUT, 'manifest.json'),
}),
// Produce the DLL CSS file
new MiniCssExtractPlugin({
filename: 'dll.css',
}),
],
// Output the DLL JS file
output: {
path: DLL_OUTPUT,
filename: 'dll.js',
library: DLL_NAME,
},
// Include a require alias for legacy UI code and styles
resolve: {
alias: {
ui: path.resolve(KIBANA_ROOT, 'src/legacy/ui/public'),
},
},
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {},
},
{ loader: 'css-loader' },
{
loader: 'string-replace-loader',
options: {
search: '__REPLACE_WITH_PUBLIC_PATH__',
replace: '/',
flags: 'g',
},
},
],
},
{
test: /\.less$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader', options: { importLoaders: 2 } },
{
loader: 'postcss-loader',
options: {
config: {
path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'),
},
},
},
{ loader: 'less-loader' },
],
},
{
test: /\.(woff|woff2|ttf|eot|svg|ico)(\?|$)/,
loader: 'file-loader',
},
],
},
};

View file

@ -5,31 +5,38 @@
*/
const path = require('path');
const devUtils = require('@kbn/dev-utils');
const storybook = require('@storybook/react/standalone');
const execa = require('execa');
// We have to start up the Kibana server to process CSS files as we change them.
// This is pretty much a hack for the moment. We can get a separate process up
// and running in the future.
execa(
process.execPath,
[
'scripts/kibana',
'--optimize.enabled=false',
'--env.name="development"',
'--plugins.initialize=false',
'--server.port=5699',
],
{
cwd: path.resolve(__dirname, '../../../..'),
stdio: ['ignore', 'inherit', 'inherit'],
buffer: false,
}
).catch(err => {
console.log('Kibana server died:', err.message);
process.exit(1);
const log = new devUtils.ToolingLog({
level: 'info',
writeTo: process.stdout,
});
const options = {
stdio: ['ignore', 'inherit', 'inherit'],
buffer: false,
};
execa.sync('node', ['storybook_dll.js'], {
cwd: __dirname,
...options,
});
// Ensure SASS has been built completely before starting Storybook
execa.sync(process.execPath, ['scripts/build_sass'], {
cwd: path.resolve(__dirname, '../../../..'),
...options,
});
// Now watch the SASS sheets for changes
execa(process.execPath, ['scripts/build_sass', '--watch'], {
cwd: path.resolve(__dirname, '../../../..'),
...options,
});
log.info('storybook: Starting Storybook');
storybook({
mode: 'dev',
port: 9001,

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
const fs = require('fs');
const path = require('path');
const execa = require('execa');
const devUtils = require('@kbn/dev-utils');
const { DLL_OUTPUT } = require('./../.storybook/constants');
const log = new devUtils.ToolingLog({
level: 'info',
writeTo: process.stdout,
});
if (fs.existsSync(DLL_OUTPUT)) {
log.info('storybook: DLL exists from previous build');
} else {
log.info('storybook: DLL missing; building');
execa.sync(
'yarn',
[
'webpack',
'--config',
'x-pack/plugins/canvas/.storybook/webpack.dll.config.js',
'--progress',
'--hide-modules',
'--display-entrypoints',
'false',
],
{
cwd: path.resolve(__dirname, '../../../..'),
stdio: ['ignore', 'inherit', 'inherit'],
buffer: false,
}
);
log.success('storybook: DLL built');
}

View file

@ -5,8 +5,25 @@
*/
const path = require('path');
const execa = require('execa');
const storybook = require('@storybook/react/standalone');
const options = {
stdio: ['ignore', 'inherit', 'inherit'],
buffer: false,
};
execa.sync('node', ['storybook_dll.js'], {
cwd: __dirname,
...options,
});
// Ensure SASS has been built completely before starting Storybook
execa.sync(process.execPath, ['scripts/build_sass'], {
cwd: path.resolve(__dirname, '../../../..'),
...options,
});
storybook({
mode: 'static',
configDir: path.resolve(__dirname, './../.storybook'),

View file

@ -4034,16 +4034,6 @@ ajv@^6.1.0, ajv@^6.5.5:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ajv@^6.1.1:
version "6.10.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==
dependencies:
fast-deep-equal "^2.0.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ajv@^6.9.1:
version "6.9.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.9.2.tgz#4927adb83e7f48e5a32b45729744c71ec39c9c7b"
@ -20213,24 +20203,16 @@ react-dev-utils@^7.0.0, react-dev-utils@^7.0.1:
strip-ansi "5.0.0"
text-table "0.2.0"
react-docgen-typescript-loader@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/react-docgen-typescript-loader/-/react-docgen-typescript-loader-3.0.0.tgz#4042e2854d29380e4d01e479d438c03ec00de8e8"
integrity sha512-xtE4bZrU9+7grFFzs8v6gWc+Wl2FCCL59hldHoX2DuQAXOmJIilUm2uPmDmRNA8RpxU1Ax+9Gl0JfUcWgx2QPA==
react-docgen-typescript-loader@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/react-docgen-typescript-loader/-/react-docgen-typescript-loader-3.1.0.tgz#09cacf872617c97f946ee920d2239f51d543be41"
integrity sha512-gY+b7RkRPty5ZN4NMQ+jwx9MzTVuIj6LJCwdWRAi1+nrHJfH2gMMytQfxFdzQ7BlgD4COWnSE8Ixtl2L62kCRw==
dependencies:
"@webpack-contrib/schema-utils" "^1.0.0-beta.0"
loader-utils "^1.1.0"
react-docgen-typescript "^1.9.0"
loader-utils "^1.2.3"
react-docgen-typescript "^1.12.3"
react-docgen-typescript-webpack-plugin@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/react-docgen-typescript-webpack-plugin/-/react-docgen-typescript-webpack-plugin-1.1.0.tgz#4bfb8c3312fce487083924842cf03f66177ab9df"
integrity sha1-S/uMMxL85IcIOSSELPA/Zhd6ud8=
dependencies:
ajv "^6.1.1"
react-docgen-typescript "^1.2.3"
react-docgen-typescript@^1.2.3, react-docgen-typescript@^1.9.0:
react-docgen-typescript@^1.12.3:
version "1.12.3"
resolved "https://registry.yarnpkg.com/react-docgen-typescript/-/react-docgen-typescript-1.12.3.tgz#fe62a5ce82e93573e316366e53adfe8273121c70"
integrity sha512-s1XswWs4ykNdWKsPyfM4qptV5dT8nnjnVi2IcjoS/vGlRNYrc0TkW0scVOrinHZ+ndKhPqA4iVNrdwhxZBzJcg==