Bug 1364894 - Upgrade from Neutrino 4 to 9 (#4216)

Neutrino controls our frontend linting, transpilation, source-maps,
testing, dev-server and optimisation of production builds.

Highlights of the upgrade are:

* Major version updates to the individual tools within (such as webpack,
  Babel and ESLint), significantly improving performance, fixing
  transpilation/minification correctness bugs, adding support for newer
  ECMAScript features, and increasing linter coverage.
* Hot reloading in the dev server now works for all entry-points and not
  just the jobs view, shortening the feedback cycle.
* Reduced bundle size due to webpack 4's tree shaking, scope hoisting,
  automatic shared/vendor code chunk splitting (no need for the manually
  maintained 'vendor' list).
* CSS is now extracted out of JS, which improves performance, reduces
  bundle size and prevents the initial white flash of un-styled content.
* Support for dynamic imports/code splitting (needed for bug 1502192).
* Support for Jest via a new Jest preset (unblocks bug 1364045).
* Support for public class field declarations (unblocks bug 1480166).
* Improved source-maps (increases the quality of production exception
  trace-backs and fixes several debugger breakpoint bugs).
* Reduced amount of custom configuration required for our fairly complex
  frontend needs, reducing maintenance burden and allowing for easier
  future Neutrino upgrades.

In addition this PR:

* Fixes the WhiteNoise `immutable_file_test()` regex, so that it now
  correctly enables browser caching of images, fonts and source maps.
* Enables webpack-dev-server's overlay feature, which displays any
  compilation errors in the browser, saving having to switch back
  to the console (this can be enabled for warnings too if desired).
* Enables webpack-dev-server's automatic browser-opening feature,
  which saves having to manually navigate to `localhost:5000` after
  running `yarn start`.
* Switches Karma tests to run Firefox in headless mode, reducing the
  workflow disruption when running `yarn test`.
* Uses the new webpack `performance` option to enable maximum asset
  file size thresholds, to help prevent bundle-size regressions.
* Rewrites the `package.json` script commands so that they now work
  correctly on Windows, even when setting environment variables.

Performance comparison:

* Local `yarn build`:
  - Cached: 2m34s -> 23s
  - Uncached: 2m34s -> 58s
* Local `yarn start`:
  - Cached: 34.5s -> 13.6s
  - Uncached: 34.5s -> 31.3s
* Local `yarn test`
  - Cached: 61.5s -> 19.8s
  - Uncached: 61.5s -> 22.0s
* Local `yarn lint`
  - Cached: 3.8s -> 1.8s
  - Uncached: 13.7s -> 13.4s
* Travis end-to-end time:
  9 minutes -> 6 minutes
* Heroku deploy end-to-end time:
  14 minutes -> 9 minutes
This commit is contained in:
Ed Morley 2018-11-02 18:48:28 +00:00 коммит произвёл GitHub
Родитель cbd0a384eb
Коммит 565ae4c13e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
35 изменённых файлов: 2838 добавлений и 3997 удалений

Просмотреть файл

@ -1,6 +1,65 @@
const path = require('path');
const Neutrino = require('neutrino');
const api = new Neutrino([path.resolve('./neutrino-custom/development.js')]);
module.exports = api.custom.eslintrc();
module.exports = {
root: true,
extends: 'eslint-config-airbnb',
parser: 'babel-eslint',
settings: {
react: {
version: '16.6',
},
},
env: {
browser: true,
jasmine: true,
},
rules: {
// TODO: Fix & remove the majority of these deviations from AirBnB style (bug 1183749).
camelcase: 'off',
'class-methods-use-this': 'off',
'consistent-return': 'off',
'default-case': 'off',
'func-names': 'off',
'function-paren-newline': 'off',
'implicit-arrow-linebreak': 'off',
// Indentation is disabled pending a switch from 4 to 2 space for JS.
indent: 'off',
'jsx-a11y/anchor-is-valid': 'off',
'jsx-a11y/click-events-have-key-events': 'off',
'jsx-a11y/label-has-associated-control': 'off',
'jsx-a11y/label-has-for': 'off',
'jsx-a11y/no-noninteractive-element-interactions': 'off',
'jsx-a11y/no-static-element-interactions': 'off',
'max-len': 'off',
'no-alert': 'off',
'no-continue': 'off',
'no-else-return': 'off',
'no-mixed-operators': 'off',
'no-nested-ternary': 'off',
'no-param-reassign': 'off',
'no-plusplus': 'off',
'no-prototype-builtins': 'off',
'no-restricted-globals': 'off',
'no-restricted-syntax': 'off',
'no-shadow': 'off',
'no-underscore-dangle': 'off',
'no-useless-escape': 'off',
'object-curly-newline': 'off',
'object-shorthand': 'off',
'operator-linebreak': 'off',
'padded-blocks': 'off',
'prefer-arrow-callback': 'off',
'prefer-destructuring': 'off',
'prefer-promise-reject-errors': 'off',
'prefer-template': 'off',
radix: 'off',
'react/button-has-type': 'off',
'react/default-props-match-prop-types': 'off',
'react/destructuring-assignment': 'off',
'react/forbid-prop-types': 'off',
'react/jsx-closing-tag-location': 'off',
'react/jsx-one-expression-per-line': 'off',
'react/jsx-wrap-multilines': 'off',
'react/no-access-state-in-setstate': 'off',
'react/no-multi-comp': 'off',
'react/no-unused-state': 'off',
},
};

2
.gitignore поставляемый
Просмотреть файл

@ -23,7 +23,7 @@ npm-error.log
# Treeherder-specific
_build/
dist/
build/
treeherder/static/
# Celery

134
.neutrinorc.js Normal file
Просмотреть файл

@ -0,0 +1,134 @@
// `use strict` is still necessary here since this file is not treated as a module.
'use strict'; // eslint-disable-line strict, lines-around-directive
const BACKEND = process.env.BACKEND || 'https://treeherder.mozilla.org';
module.exports = {
options: {
source: 'ui/',
mains: {
index: {
entry: 'job-view/index.jsx',
favicon: 'ui/img/tree_open.png',
title: 'Treeherder',
},
logviewer: {
entry: 'entry-logviewer.js',
template: 'ui/logviewer.html',
},
userguide: {
entry: 'userguide/index.jsx',
favicon: 'ui/img/tree_open.png',
title: 'Treeherder User Guide',
},
login: {
entry: 'login-callback/index.jsx',
title: 'Treeherder Login',
},
testview: {
entry: 'test-view/index.jsx',
title: 'Treeherder Test View',
},
perf: {
entry: 'entry-perf.js',
template: 'ui/perf.html',
},
'intermittent-failures': {
entry: 'intermittent-failures/index.jsx',
title: 'Intermittent Failures View',
},
},
tests: 'tests/ui/',
},
use: [
process.env.NODE_ENV === 'development' && ['@neutrinojs/eslint', {
eslint: {
// Treat ESLint errors as warnings so they don't block the webpack build.
// Remove if/when changed in @neutrinojs/eslint.
emitWarning: true,
// We manage our lint config in .eslintrc.js instead of here.
useEslintrc: true,
},
}],
['@neutrinojs/react', {
devServer: {
historyApiFallback: false,
open: !process.env.MOZ_HEADLESS,
// Remove when enabled by default (https://github.com/neutrinojs/neutrino/issues/1131).
overlay: true,
proxy: {
// Proxy any paths not recognised by webpack to the specified backend.
'*': {
changeOrigin: true,
headers: {
// Prevent Django CSRF errors, whilst still making it clear
// that the requests were from local development.
referer: `${BACKEND}/webpack-dev-server`,
},
target: BACKEND,
onProxyRes: (proxyRes) => {
// Strip the cookie `secure` attribute, otherwise production's cookies
// will be rejected by the browser when using non-HTTPS localhost:
// https://github.com/nodejitsu/node-http-proxy/pull/1166
const removeSecure = str => str.replace(/; secure/i, '');
const cookieHeader = proxyRes.headers['set-cookie'];
if (cookieHeader) {
proxyRes.headers['set-cookie'] = Array.isArray(cookieHeader)
? cookieHeader.map(removeSecure)
: removeSecure(cookieHeader);
}
},
},
},
// Inside Vagrant filesystem watching has to be performed using polling mode,
// since inotify doesn't work with Virtualbox shared folders.
watchOptions: process.env.USE_WATCH_POLLING && {
// Poll only once a second and ignore the node_modules folder to keep CPU usage down.
poll: 1000,
ignored: /node_modules/,
},
},
devtool: {
// Enable source maps for `yarn build` too (but not on CI, since it doubles build times).
production: process.env.CI ? false : 'source-map',
},
style: {
// Disable Neutrino's CSS modules support, since we don't use it.
modules: false,
},
targets: {
browsers: [
'last 1 Chrome versions',
'last 1 Edge versions',
'last 1 Firefox versions',
'last 1 Safari versions',
],
},
}],
['@neutrinojs/copy', {
patterns: [
'ui/contribute.json',
'ui/revision.txt',
'ui/robots.txt',
],
}],
(neutrino) => {
neutrino.config
.plugin('provide')
.use(require.resolve('webpack/lib/ProvidePlugin'), [{
// Required since AngularJS and jquery.flot don't import jQuery themselves.
jQuery: 'jquery',
'window.jQuery': 'jquery',
}]);
if (process.env.NODE_ENV === 'production') {
// Fail the build if these file size thresholds (in bytes) are exceeded,
// to help prevent unknowingly regressing the bundle size (bug 1384255).
neutrino.config.performance
.hints('error')
.maxAssetSize(1.29 * 1024 * 1024)
.maxEntrypointSize(1.63 * 1024 * 1024);
}
},
],
};

Просмотреть файл

@ -15,8 +15,9 @@ matrix:
install:
- source ./bin/travis-setup.sh js_env
script:
# `yarn build` is tested as part of the Selenium job.
- yarn lint
- yarn test
- yarn build
- env: python2-linters
sudo: false

Просмотреть файл

@ -5,23 +5,16 @@
set -euo pipefail
# Make the current Git revision accessible at <site-root>/revision.txt
echo "$SOURCE_VERSION" > ui/revision.txt
# Create a `dist/` directory containing built/minified versions of the `ui/` assets.
# Uses the node binaries/packages installed by the nodejs buildpack previously.
yarn build
echo "$SOURCE_VERSION" > build/revision.txt
# Generate gzipped versions of files that would benefit from compression, that
# WhiteNoise can then serve in preference to the originals. This is required
# since WhiteNoise's Django storage backend only gzips assets handled by
# collectstatic, and so does not affect files in the `dist/` directory.
python -m whitenoise.compress dist
# collectstatic, and so does not affect files in the `build/` directory.
python -m whitenoise.compress build
# Remove nodejs files to reduce slug size (and avoid environment variable
# pollution from the nodejs profile script), since they are no longer
# required once `yarn build` has run. The buildpack cache will still
# contain them, so this doesn't slow down the next slug compile.
rm -r .heroku/node/
rm -r .heroku/yarn/
rm -r .profile.d/nodejs.sh
rm -r node_modules/
# required after `yarn heroku-postbuild` has run. The buildpack cache will
# still contain them, so this doesn't slow down the next slug compile.
rm -r .heroku/node/ .heroku/yarn/ .profile.d/nodejs.sh node_modules/

Просмотреть файл

@ -47,7 +47,7 @@ production site. You do not need to set up the Vagrant VM unless making backend
If you need to serve data from another domain, type:
```bash
$ BACKEND_DOMAIN=<url> yarn start
$ BACKEND=<url> yarn start
```
This will run the unminified UI using ``<url>`` as the service domain.
@ -153,8 +153,7 @@ Starting a local Treeherder instance
vagrant ~/treeherder$ yarn start:local
```
This will build the UI code in the ``dist/`` folder and keep watching for
new changes.
This will build the UI code and keep watching for new changes.
* Visit <http://localhost:5000> in your browser (NB: port has changed). Note: There will be no data to display until the ingestion tasks are run.

53
karma.conf.js Normal file
Просмотреть файл

@ -0,0 +1,53 @@
// We're not using @neutrinojs/karma since we'd end up overriding most of it in
// order to use Firefox instead of Chrome, jasmine instead of mocha, and so on.
const neutrino = require('neutrino');
process.env.NODE_ENV = 'test';
const webpackConfig = neutrino().webpack();
// Skip building the entrypoints, since everything is imported in tests/ui/unit/init.js.
delete webpackConfig.entry;
// Re-enable Buffer since Karma fails to work without it.
webpackConfig.node.Buffer = true;
// Work around karma-webpack hanging under webpack 4:
// https://github.com/webpack-contrib/karma-webpack/issues/322
webpackConfig.optimization.splitChunks = false;
webpackConfig.optimization.runtimeChunk = false;
module.exports = (config) => {
config.set({
plugins: [
'karma-webpack',
'karma-firefox-launcher',
'karma-jasmine',
],
browsers: ['FirefoxHeadless'],
frameworks: ['jasmine'],
files: [
'tests/ui/unit/init.js',
{
pattern: 'tests/ui/mock/**/*.json',
watched: true,
included: false,
served: true,
},
],
preprocessors: {
'tests/ui/unit/init.js': ['webpack'],
},
webpack: webpackConfig,
webpackMiddleware: {
// Make the webpack compile output less verbose.
stats: {
all: false,
errors: true,
timings: true,
warnings: true,
},
},
});
};

Просмотреть файл

@ -1,326 +0,0 @@
'use strict';
const path = require('path');
const webpack = require('webpack');
const lintPreset = require('./lint');
const reactPreset = require('neutrino-preset-react');
const HtmlPlugin = require('html-webpack-plugin');
const htmlTemplate = require('html-webpack-template');
const CopyPlugin = require('copy-webpack-plugin');
const CWD = process.cwd();
const SRC = path.join(CWD, 'src'); // neutrino's default source directory
const UI = path.join(CWD, 'ui');
const DIST = path.join(CWD, 'dist');
const INDEX_TEMPLATE = path.join(UI, 'index.html');
const PERF_TEMPLATE = path.join(UI, 'perf.html');
const LOGVIEWER_TEMPLATE = path.join(UI, 'logviewer.html');
const HTML_MINIFY_OPTIONS = {
useShortDoctype: true,
keepClosingSlash: true,
collapseWhitespace: true,
preserveLineBreaks: true
};
module.exports = neutrino => {
reactPreset(neutrino);
lintPreset(neutrino);
// Change the ouput path from build/ to dist/:
neutrino.config.output.path(DIST);
if (process.env.NODE_ENV !== 'test') {
// Include files from node_modules in the separate, more-cacheable vendor chunk:
const jsDeps = [
'auth0-js',
'bootstrap',
'hawk',
'jquery',
'jquery.scrollto',
'js-yaml',
'metrics-graphics',
'mousetrap',
'numeral',
'prop-types',
'react',
'react-dom',
'react-highlight-words',
'react-select',
'taskcluster-client-web',
'taskcluster-lib-scopes'
];
jsDeps.map(dep =>
neutrino.config.entry('vendor').add(dep)
);
}
// Neutrino looks for the entry at src/index.js by default; Delete this and add the index in ui/:
neutrino.config
.entry('index')
.delete(path.join(SRC, 'index.js'))
.add(path.join(UI, 'job-view', 'index.jsx'))
.end();
// Add several other treeherder entry points:
neutrino.config
.entry('perf')
.add(path.join(UI, 'entry-perf.js'))
.end();
neutrino.config
.entry('logviewer')
.add(path.join(UI, 'entry-logviewer.js'))
.end();
neutrino.config
.entry('login')
.add(path.join(UI, 'login-callback', 'index.jsx'))
.end();
neutrino.config
.entry('userguide')
.add(path.join(UI, 'userguide', 'index.jsx'))
.end();
neutrino.config
.entry('testview')
.add(path.join(UI, 'test-view', 'index.jsx'))
.end();
neutrino.config
.entry('intermittent-failures')
.add(path.join(UI, 'intermittent-failures', 'index.jsx'))
.end();
// Likewise, we must modify the include paths for the compile rule to look in ui/ instead of src/:
neutrino.config
.module
.rule('compile')
.include(UI);
// Don't use file loader for html...
// https://github.com/mozilla-neutrino/neutrino-dev/blob/v4.2.0/packages/neutrino-preset-web/src/index.js#L64-L69
neutrino.config
.module
.rule('html')
.loaders.delete('file');
// Instead, use html-loader, like Neutrino 8:
// https://github.com/mozilla-neutrino/neutrino-dev/blob/v8.0.18/packages/html-loader/index.js#L7
neutrino.config
.module
.rule('html')
.loader('html', require.resolve('html-loader'), {
// Override html-loader's default of `img:src`,
// so it also parses favicon images (`<link href="...">`).
attrs: ['img:src', 'link:href']
});
// Backport Neutrino 8's `test` regex, since Neutrino 4 omitted `.gif`:
// https://github.com/mozilla-neutrino/neutrino-dev/blob/v4.2.0/packages/neutrino-preset-web/src/index.js#L108
// https://github.com/mozilla-neutrino/neutrino-dev/blob/v8.0.18/packages/image-loader/index.js#L20
// Fixes "You may need an appropriate loader to handle this file type" errors for `dancing_cat.gif`.
neutrino.config
.module
.rule('img')
.test(/\.(png|jpg|jpeg|gif|webp)(\?v=\d+\.\d+\.\d+)?$/);
// Remove Neutrino 4's invalid SVG mimetype, and use auto-detection instead, like Neutrino 8.
// https://github.com/mozilla-neutrino/neutrino-dev/blob/v4.2.0/packages/neutrino-preset-web/src/index.js#L98-L104
// https://github.com/mozilla-neutrino/neutrino-dev/blob/v8.0.18/packages/image-loader/index.js#L11-L16
// Fixes the log viewer icon on the job details panel (which url-loader embeds as a base64 encoded data URI).
neutrino.config
.module
.rule('svg')
.loader('url', ({ options }) => {
options.mimetype = null;
return { options };
});
// Set up templates for each entry point:
neutrino.config.plugins.delete('html');
neutrino.config
.plugin('html-index')
.use(HtmlPlugin, {
inject: 'body',
template: INDEX_TEMPLATE,
chunks: ['index', 'vendor', 'manifest'],
minify: HTML_MINIFY_OPTIONS
});
neutrino.config
.plugin('html-perf')
.use(HtmlPlugin, {
inject: 'body',
filename: 'perf.html',
template: PERF_TEMPLATE,
chunks: ['perf', 'vendor', 'manifest'],
minify: HTML_MINIFY_OPTIONS
});
neutrino.config
.plugin('html-logviewer')
.use(HtmlPlugin, {
inject: 'body',
filename: 'logviewer.html',
template: LOGVIEWER_TEMPLATE,
chunks: ['logviewer', 'vendor', 'manifest'],
minify: HTML_MINIFY_OPTIONS
});
neutrino.config
.plugin('html-userguide')
.use(HtmlPlugin, {
inject: false,
template: htmlTemplate,
filename: 'userguide.html',
favicon: 'ui/img/tree_open.png',
chunks: ['userguide', 'vendor', 'manifest'],
appMountId: 'root',
xhtml: true,
mobile: true,
minify: HTML_MINIFY_OPTIONS,
title: 'Treeherder User Guide'
});
neutrino.config
.plugin('html-login')
.use(HtmlPlugin, {
inject: false,
template: htmlTemplate,
filename: 'login.html',
chunks: ['login', 'vendor', 'manifest'],
appMountId: 'root',
xhtml: true,
mobile: true,
minify: HTML_MINIFY_OPTIONS,
title: 'Treeherder Login',
meta: [
{
name: 'description',
content: 'Treeherder Login',
},
{
name: 'author',
content: 'Mozilla Treeherder',
},
],
});
neutrino.config
.plugin('html-testview')
.use(HtmlPlugin, {
inject: false,
template: htmlTemplate,
filename: 'testview.html',
chunks: ['testview', 'vendor', 'manifest'],
appMountId: 'root',
xhtml: true,
mobile: true,
minify: HTML_MINIFY_OPTIONS,
title: "Treeherder TestView",
meta: [
{
"name": "description",
"content": "Treeherder TestView"
},
{
"name": "author",
"content": "Mozilla Treeherder"
}
]
});
neutrino.config
.plugin('html-index')
.use(HtmlPlugin, {
inject: false,
template: htmlTemplate,
filename: 'index.html',
favicon: 'ui/img/tree_open.png',
chunks: ['index', 'vendor', 'manifest'],
appMountId: 'root',
xhtml: true,
mobile: true,
minify: HTML_MINIFY_OPTIONS,
title: 'Treeherder',
meta: [
{
name: 'description',
content: 'Treeherder',
},
{
name: 'author',
content: 'Mozilla Treeherder',
},
],
});
neutrino.config
.plugin('html-intermittent-failures')
.use(HtmlPlugin, {
inject: false,
template: htmlTemplate,
filename: 'intermittent-failures.html',
chunks: ['intermittent-failures', 'vendor', 'manifest'],
appMountId: 'root',
xhtml: true,
mobile: true,
minify: HTML_MINIFY_OPTIONS,
title: "Treeherder Intermittent Failures",
meta: [
{
"name": "description",
"content": "Treeherder Intermittent Failures"
},
{
"name": "author",
"content": "Mozilla Treeherder"
}
]
});
// Adjust babel env to loosen up browser compatibility requirements
neutrino.config
.module
.rule('compile')
.loader('babel', ({ options }) => {
options.presets[0][1].targets.browsers = [
'last 1 Chrome versions',
'last 1 Firefox versions',
'last 1 Edge versions',
'last 1 Safari versions'
];
// Work around a transform-regenerator bug that causes "Cannot read property '0' of null"
// when encountering usages of async. See:
// https://github.com/babel/babel/issues/4759
options.presets[0][1].include = options.presets[0][1].include.filter(e => e !== 'transform-regenerator');
return options;
});
// Remove additional node_modules directories added by Neutrino that cause
// incorrect module resolution, and restore the webpack defaults:
// https://bugzilla.mozilla.org/show_bug.cgi?id=1465041
// https://github.com/mozilla-neutrino/neutrino-dev/issues/822
// https://webpack.js.org/configuration/resolve/#resolve-modules
neutrino.config.resolve.modules.clear();
neutrino.config.resolve.modules.add('node_modules');
neutrino.config.resolveLoader.modules.clear();
neutrino.config.resolveLoader.modules.add('node_modules');
neutrino.config
.plugin('provide')
.use(webpack.ProvidePlugin, {
// Required since AngularJS and jquery.flot don't import jQuery themselves.
jQuery: require.resolve('jquery'),
'window.jQuery': require.resolve('jquery'),
});
neutrino.config.devtool('source-map');
// Overwriting the existing copy rule rather than modifying, since it's only
// enabled in production, whereas it really should be used in development too.
neutrino.config.plugin('copy')
.use(CopyPlugin, [
{ context: UI, from: 'contribute.json' },
{ context: UI, from: 'revision.txt' },
{ context: UI, from: 'robots.txt' },
]);
};
module.exports.CWD = CWD;
module.exports.UI = UI;
module.exports.DIST = DIST;

Просмотреть файл

@ -1,48 +0,0 @@
'use strict';
const basePreset = require('./base');
const UI = require('./base').UI;
// Default to the production backend (is overridden to localhost when using start:local)
const BACKEND_DOMAIN = process.env.BACKEND_DOMAIN || 'https://treeherder.mozilla.org';
module.exports = neutrino => {
basePreset(neutrino);
// Make the dev server proxy any paths not recognised by webpack to the specified backend.
neutrino.config.devServer
.contentBase(UI)
.set('proxy', {
'*': {
target: BACKEND_DOMAIN,
changeOrigin: true,
onProxyReq: (proxyReq) => {
// Adjust the referrer to keep Django's CSRF protection happy, whilst
// still making it clear that the requests were from local development.
proxyReq.setHeader('referer', `${BACKEND_DOMAIN}/webpack-dev-server`);
},
onProxyRes: (proxyRes) => {
// Strip the cookie `secure` attribute, otherwise prod cookies
// will be rejected by the browser when using non-HTTPS localhost:
// https://github.com/nodejitsu/node-http-proxy/pull/1166
const removeSecure = str => str.replace(/; secure/i, '');
const setCookie = proxyRes.headers['set-cookie'];
if (setCookie) {
const result = Array.isArray(setCookie)
? setCookie.map(removeSecure)
: removeSecure(setCookie);
proxyRes.headers['set-cookie'] = result;
}
}
}
});
if (process.env.USE_WATCH_POLLING) {
// Inside Vagrant filesystem watching has to be performed using polling mode,
// since inotify doesn't work with Virtualbox shared folders.
neutrino.config.devServer.set('watchOptions', {
// Poll only once a second and ignore the node_modules folder to keep CPU usage down.
poll: 1000,
ignored: /node_modules/,
});
}
};

Просмотреть файл

@ -1,105 +0,0 @@
// `use strict` is still necessary here since this file is not treated as a module.
'use strict'; // eslint-disable-line strict, lines-around-directive
const merge = require('deepmerge');
const lintBase = require('neutrino-lint-base');
const path = require('path');
const CWD = process.cwd();
const UI = path.join(CWD, 'ui');
module.exports = (neutrino) => {
lintBase(neutrino);
neutrino.config.module
.rule('lint')
.include(UI)
.test(/\.jsx?$/)
.loader('eslint', props => merge(props, {
options: {
plugins: ['react'],
envs: ['browser', 'es6', 'commonjs', 'jasmine'],
baseConfig: {
extends: ['airbnb'],
},
rules: {
// TODO: Fix & remove these deviations from AirBnB style (bug 1183749).
camelcase: 'off',
'class-methods-use-this': 'off',
'consistent-return': 'off',
'default-case': 'off',
'func-names': 'off',
// Indentation is disabled pending a switch from 4 to 2 space for JS.
indent: 'off',
'jsx-a11y/label-has-for': 'off',
'jsx-a11y/no-noninteractive-element-interactions': 'off',
'jsx-a11y/no-static-element-interactions': 'off',
'max-len': 'off',
'no-alert': 'off',
'no-continue': 'off',
'no-loop-func': 'off',
'no-mixed-operators': 'off',
'no-nested-ternary': 'off',
'no-param-reassign': 'off',
'no-plusplus': 'off',
'no-prototype-builtins': 'off',
'no-restricted-syntax': 'off',
'no-shadow': 'off',
'no-underscore-dangle': 'off',
'no-useless-escape': 'off',
'object-shorthand': 'off',
'padded-blocks': 'off',
'prefer-arrow-callback': 'off',
'prefer-template': 'off',
radix: 'off',
'react/forbid-prop-types': 'off',
'react/no-multi-comp': 'off',
// Backport of:
// https://github.com/airbnb/javascript/blob/eslint-config-airbnb-v17.1.0/packages/eslint-config-airbnb/rules/react.js#L230-L272
// Remove once we're on eslint-config-airbnb v17.
'react/sort-comp': ['error', {
order: [
'static-methods',
'instance-variables',
'lifecycle',
'/^on.+$/',
'getters',
'setters',
'/^(get|set)(?!(InitialState$|DefaultProps$|ChildContext$)).+$/',
'instance-methods',
'everything-else',
'rendering',
],
groups: {
lifecycle: [
'displayName',
'propTypes',
'contextTypes',
'childContextTypes',
'mixins',
'statics',
'defaultProps',
'constructor',
'getDefaultProps',
'getInitialState',
'state',
'getChildContext',
'componentWillMount',
'componentDidMount',
'componentWillReceiveProps',
'shouldComponentUpdate',
'componentWillUpdate',
'componentDidUpdate',
'componentWillUnmount',
],
rendering: [
'/^render.+$/',
'render',
],
},
}],
},
// Remove once we're on newer ESLint that has the updated browser globals list.
globals: ['AbortController'],
},
}));
};

Просмотреть файл

@ -1,26 +0,0 @@
'use strict';
const basePreset = require('./base');
const CleanPlugin = require('clean-webpack-plugin');
const CWD = require('./base').CWD;
const DIST = require('./base').DIST;
module.exports = neutrino => {
basePreset(neutrino);
neutrino.config.plugin('minify')
.inject(BabiliPlugin => new BabiliPlugin({
evaluate: false, // prevents some minification errors
// Prevents a minification error in react-dom that manifests as
// `ReferenceError: Hp is not defined` when loading the main jobs view (bug 1426902).
// TODO: Either remove this workaround or file upstream if this persists
// after the Neutrino upgrade (which comes with latest babel-plugin-minify-mangle-names).
mangle: {
keepFnName: true,
},
}
));
neutrino.config.plugin('clean')
.use(CleanPlugin, [DIST], { root: CWD } );
};

Просмотреть файл

@ -1,26 +0,0 @@
'use strict';
const basePreset = require('./base');
const karmaPreset = require('neutrino-preset-karma');
module.exports = neutrino => {
basePreset(neutrino);
karmaPreset(neutrino);
// Normal karma config
neutrino.custom.karma = {
browsers: ['Firefox'],
plugins: [
require.resolve('karma-webpack'),
require.resolve('karma-firefox-launcher'),
require.resolve('karma-jasmine'),
],
frameworks: ['jasmine'],
files: [
'tests/ui/unit/init.js',
{ pattern: 'tests/ui/mock/**/*.json', watched: true, served: true, included: false }
],
preprocessors: {
'tests/ui/unit/init.js': ['webpack'],
},
};
};

Просмотреть файл

@ -14,6 +14,8 @@
"d3": "5.7.0"
},
"dependencies": {
"@neutrinojs/copy": "9.0.0-beta.1",
"@neutrinojs/react": "9.0.0-beta.1",
"@types/angular": "*",
"@types/prop-types": "*",
"@types/react": "*",
@ -28,15 +30,8 @@
"auth0-js": "9.8.1",
"bootstrap": "4.1.3",
"d3": "5.7.0",
"deepmerge": "1.5.2",
"eslint": "3.19.0",
"eslint-config-airbnb": "15.1.0",
"eslint-plugin-import": "2.14.0",
"eslint-plugin-jsx-a11y": "5.1.1",
"eslint-plugin-react": "7.11.1",
"font-awesome": "4.7.0",
"history": "4.7.2",
"html-loader": "0.5.5",
"jquery": "3.3.1",
"jquery.flot": "0.8.3",
"jquery.scrollto": "2.1.2",
@ -48,9 +43,7 @@
"metrics-graphics": "2.15.6",
"moment": "2.22.2",
"mousetrap": "1.6.2",
"neutrino": "4.3.1",
"neutrino-lint-base": "4.3.1",
"neutrino-preset-react": "4.2.3",
"neutrino": "9.0.0-beta.1",
"ng-text-truncate-2": "1.0.1",
"numeral": "2.0.6",
"popper.js": "1.14.4",
@ -61,7 +54,7 @@
"react-dom": "16.6.0",
"react-fontawesome": "1.6.1",
"react-highlight-words": "0.14.0",
"react-hot-loader": "3.1.3",
"react-hot-loader": "4.3.12",
"react-hotkeys": "1.1.4",
"react-linkify": "0.2.2",
"react-redux": "5.1.0",
@ -76,27 +69,38 @@
"redux-debounce": "1.0.1",
"redux-thunk": "2.3.0",
"taskcluster-client-web": "8.0.4",
"taskcluster-lib-scopes": "10.0.1"
"taskcluster-lib-scopes": "10.0.1",
"webpack": "4.24.0",
"webpack-cli": "3.1.2"
},
"devDependencies": {
"@neutrinojs/eslint": "9.0.0-beta.1",
"angular-mocks": "1.7.5",
"enzyme": "3.7.0",
"enzyme-adapter-react-16": "1.6.0",
"eslint": "5.8.0",
"eslint-config-airbnb": "17.1.0",
"eslint-plugin-import": "2.14.0",
"eslint-plugin-jsx-a11y": "6.1.2",
"eslint-plugin-react": "7.11.1",
"fetch-mock": "7.2.2",
"jasmine-core": "3.3.0",
"jasmine-jquery": "2.1.1",
"karma": "1.7.1",
"karma": "3.1.1",
"karma-firefox-launcher": "1.1.0",
"karma-jasmine": "1.1.2",
"neutrino-preset-karma": "4.2.1"
"karma-webpack": "4.0.0-beta.0",
"webpack-dev-server": "3.1.10"
},
"scripts": {
"build": "node ./node_modules/neutrino/bin/neutrino build --presets ./neutrino-custom/production.js",
"lint": "node ./node_modules/eslint/bin/eslint.js --cache --ext js,jsx \".*.js\" \"*.js\" ui/ tests/ui/ neutrino-custom/lint.js",
"start": "node ./node_modules/neutrino/bin/neutrino start --presets ./neutrino-custom/development.js",
"start:local": "BACKEND_DOMAIN=http://localhost:8000 node ./node_modules/neutrino/bin/neutrino start --presets ./neutrino-custom/development.js",
"start:stage": "BACKEND_DOMAIN=https://treeherder.allizom.org node ./node_modules/neutrino/bin/neutrino start --presets ./neutrino-custom/development.js",
"test": "node ./node_modules/neutrino/bin/neutrino test --presets ./neutrino-custom/test.js",
"test:watch": "node ./node_modules/neutrino/bin/neutrino test --watch --presets ./neutrino-custom/test.js"
"build": "node ./node_modules/webpack/bin/webpack.js --mode production",
"build:dev": "node ./node_modules/webpack/bin/webpack.js --mode development",
"heroku-postbuild": "yarn build",
"lint": "node ./node_modules/eslint/bin/eslint.js --cache --report-unused-disable-directives --format codeframe --ext js,jsx \".*.js\" \"*.js\" ui/ tests/ui/",
"start": "node ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --mode development",
"start:local": "node ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --mode development --env.BACKEND=http://localhost:8000",
"start:stage": "node ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --mode development --env.BACKEND=https://treeherder.allizom.org",
"test": "node ./node_modules/karma/bin/karma start --single-run",
"test:watch": "node ./node_modules/karma/bin/karma start"
}
}

Просмотреть файл

@ -10,18 +10,7 @@
"@types/prop-types",
"@types/react",
"@types/react-dom",
"@uirouter/angularjs",
"deepmerge",
"eslint",
"eslint-config-airbnb",
"eslint-plugin-jsx-a11y",
"html-loader",
"karma",
"neutrino",
"neutrino-lint-base",
"neutrino-preset-karma",
"neutrino-preset-react",
"react-hot-loader"
"@uirouter/angularjs"
],
"reviewers": [
"@mozilla/treeherder-admins"

Просмотреть файл

@ -32,7 +32,7 @@ filterwarnings =
error
ignore::ImportWarning
ignore::PendingDeprecationWarning
# WhiteNoise warns if either `treeherder/static/` or `dist/` do not exist at startup,
# WhiteNoise warns if either `treeherder/static/` or `build/` do not exist at startup,
# however this is expected when running tests since Django collectstatic and yarn build
# (which create those directories) typically aren't run apart from during deployments.
ignore:No directory at.*:UserWarning:whitenoise.base

41
tests/test_middleware.py Normal file
Просмотреть файл

@ -0,0 +1,41 @@
import pytest
from treeherder.middleware import CustomWhiteNoise
URLS_IMMUTABLE = [
# Assets generated by Neutrino.
'/assets/2.379789df.css',
'/assets/dancing_cat.fa5552a5.gif',
'/assets/fontawesome-webfont.af7ae505.woff2',
'/assets/fontawesome-webfont.fee66e71.woff',
'/assets/index.1d85033a.js',
'/assets/index.1d85033a.js.map',
'/assets/perf.d7fea1e4.css',
'/assets/perf.d7fea1e4.css.map',
'/assets/treeherder-logo.3df97cff.png',
]
URLS_NOT_IMMUTABLE = [
'/',
'/contribute.json',
'/perf.html',
'/revision.txt',
'/tree_open.png',
'/docs/schema.js',
# The unhashed Neutrino/webpack output if using `yarn build --mode development`.
'/assets/runtime.js',
'/assets/vendors~index.js',
# The unhashed Django static asset originals (used in development).
'/static/debug_toolbar/assets/toolbar.css',
'/static/rest_framework/docs/js/jquery.json-view.min.js',
]
@pytest.mark.parametrize('url', URLS_IMMUTABLE)
def test_immutable_file_test_matches(url):
assert CustomWhiteNoise().immutable_file_test('', url)
@pytest.mark.parametrize('url', URLS_NOT_IMMUTABLE)
def test_immutable_file_test_does_not_match(url):
assert not CustomWhiteNoise().immutable_file_test('', url)

Просмотреть файл

@ -1,6 +1,5 @@
// Karma/webpack entry for tests
import jQuery from 'jquery';
// Manually import angular since angular-mocks doesn't do so itself
import 'angular';
import 'angular-mocks';
@ -8,11 +7,6 @@ import 'jasmine-jquery';
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
// Global variables are set here instead of with webpack.ProvidePlugin
// because neutrino removes plugin definitions for karma runs:
// https://github.com/mozilla-neutrino/neutrino-dev/issues/617
window.jQuery = jQuery;
configure({ adapter: new Adapter() });
const jsContext = require.context('../../../ui/js', true, /^\.\/.*\.jsx?$/);

Просмотреть файл

@ -386,7 +386,7 @@ REST_FRAMEWORK = {
# Whitenoise
# Files in this directory will be served by WhiteNoise at the site root.
WHITENOISE_ROOT = os.path.join(PROJECT_DIR, "..", "dist")
WHITENOISE_ROOT = os.path.join(PROJECT_DIR, "..", "build")
# Serve index.html for URLs ending in a trailing slash.
WHITENOISE_INDEX_FILE = True
# Only output the hashed filename version of static files and not the originals.

Просмотреть файл

@ -6,15 +6,20 @@ from whitenoise.middleware import WhiteNoiseMiddleware
class CustomWhiteNoise(WhiteNoiseMiddleware):
"""Sets long max-age headers for webpack generated files."""
"""Sets long max-age headers for Neutrino-generated hashed files."""
# Matches webpack's style of chunk filenames. eg:
# index.f03882a6258f16fceb70.bundle.js
IMMUTABLE_FILE_RE = re.compile(r'\.[a-f0-9]{16,}\.bundle\.(js|css)$')
# Matches Neutrino's style of hashed filename URLs, eg:
# /assets/index.1d85033a.js
# /assets/2.379789df.css.map
# /assets/fontawesome-webfont.af7ae505.woff2
IMMUTABLE_FILE_RE = re.compile(r'^/assets/.*\.[a-f0-9]{8}\..*')
def immutable_file_test(self, path, url):
"""Support webpack bundle filenames when setting long max-age headers."""
if self.IMMUTABLE_FILE_RE.search(url):
"""
Determines whether the given URL represents an immutable file (i.e. a file with a
hash of its contents as part of its name) which can therefore be cached forever.
"""
if self.IMMUTABLE_FILE_RE.match(url):
return True
# Otherwise fall back to the default method, so we catch filenames in the
# style output by GzipManifestStaticFilesStorage during collectstatic. eg:

Просмотреть файл

@ -22,9 +22,6 @@ import './css/treeherder-loading-overlay.css';
// Bootstrap the Angular modules against which everything will be registered
import './js/perf';
// React UI
import './shared/auth/Login';
// Perf JS
import './js/filters';
import './js/models/perf/issue_tracker';

Просмотреть файл

@ -272,7 +272,7 @@ export const phTimeRangeValues = {
export const phDefaultFramework = 'talos';
export const phFrameworksWithRelatedBranches = [
1, // talos
1, // talos
10, // raptor
11, // js-bench
12, // devtools

Просмотреть файл

@ -1,5 +1,6 @@
import React from 'react';
import { HashRouter, Route, Switch, Redirect } from 'react-router-dom';
import { hot } from 'react-hot-loader';
import MainView from './MainView';
import BugDetailsView from './BugDetailsView';
@ -57,4 +58,4 @@ class App extends React.Component {
}
}
export default App;
export default hot(module)(App);

Просмотреть файл

@ -1,6 +1,5 @@
import React from 'react';
import { render } from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'font-awesome/css/font-awesome.css';
import 'react-table/react-table.css';
@ -9,16 +8,4 @@ import '../css/treeherder-global.css';
import '../css/intermittent-failures.css';
import App from './App';
function load() {
render((
<AppContainer>
<App />
</AppContainer>
), document.getElementById('root'));
}
load();
if (module.hot) {
module.hot.accept('./App', () => load(App));
}
render(<App />, document.getElementById('root'));

Просмотреть файл

@ -1,4 +1,5 @@
import React from 'react';
import { hot } from 'react-hot-loader';
import SplitPane from 'react-split-pane';
import { thFavicons } from '../helpers/constants';
@ -313,4 +314,4 @@ class App extends React.Component {
}
}
export default App;
export default hot(module)(App);

Просмотреть файл

@ -174,5 +174,5 @@ ActiveFilters.propTypes = {
filterBarFilters: PropTypes.array.isRequired,
isFieldFilterVisible: PropTypes.bool.isRequired,
toggleFieldFilterVisible: PropTypes.func.isRequired,
classificationTypes: PropTypes.array.isRequired, // eslint-disable-line react/no-unused-prop-types
classificationTypes: PropTypes.array.isRequired,
};

Просмотреть файл

@ -1,6 +1,5 @@
import React from 'react';
import { render } from 'react-dom';
import { AppContainer } from 'react-hot-loader';
// Vendor Styles
import 'bootstrap/dist/css/bootstrap.min.css';
@ -25,14 +24,4 @@ import '../css/treeherder-loading-overlay.css';
import App from './App';
const load = () => render((
<AppContainer>
<App />
</AppContainer>
), document.getElementById('root'));
if (module.hot) {
module.hot.accept('./App', load);
}
load();
render(<App />, document.getElementById('root'));

Просмотреть файл

@ -168,7 +168,7 @@ class PushJobs extends React.Component {
PushJobs.propTypes = {
push: PropTypes.object.isRequired,
platforms: PropTypes.array.isRequired, // eslint-disable-line react/no-unused-prop-types
platforms: PropTypes.array.isRequired,
repoName: PropTypes.string.isRequired,
filterModel: PropTypes.object.isRequired,
togglePinJob: PropTypes.func.isRequired,

Просмотреть файл

@ -1,5 +1,6 @@
import React from 'react';
import Icon from 'react-fontawesome';
import { hot } from 'react-hot-loader';
import AuthService from '../shared/auth/AuthService';
import { webAuth, parseHash } from '../helpers/auth';
@ -74,4 +75,4 @@ class LoginCallback extends React.PureComponent {
}
}
export default LoginCallback;
export default hot(module)(LoginCallback);

Просмотреть файл

@ -1,19 +1,8 @@
import React from 'react';
import { render } from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import 'font-awesome/css/font-awesome.css';
import LoginCallback from './LoginCallback';
import '../css/login.css';
const load = () => render((
<AppContainer>
<LoginCallback />
</AppContainer>
), document.getElementById('root'));
if (module.hot) {
module.hot.accept('./LoginCallback', load);
}
load();
render(<LoginCallback />, document.getElementById('root'));

Просмотреть файл

@ -1,7 +1,6 @@
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { AppContainer } from 'react-hot-loader';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'font-awesome/css/font-awesome.css';
@ -9,16 +8,8 @@ import '../css/treeherder-test-view.css';
import { store, actions } from './redux/store';
import App from './ui/App';
const load = () => render((
<AppContainer>
<Provider store={store}>
<App actions={actions} />
</Provider>
</AppContainer>
render((
<Provider store={store}>
<App actions={actions} />
</Provider>
), document.getElementById('root'));
if (module.hot) {
module.hot.accept('./ui/App', load);
}
load();

Просмотреть файл

@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { hot } from 'react-hot-loader';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Navigation from './Navigation';
@ -41,4 +42,4 @@ App.defaultProps = {
location: null,
};
export default App;
export default hot(module)(App);

Просмотреть файл

@ -1,4 +1,5 @@
import React from 'react';
import { hot } from 'react-hot-loader';
import UserGuideHeader from './UserGuideHeader';
import UserGuideBody from './UserGuideBody';
@ -14,4 +15,4 @@ const App = () => (
</div>
);
export default App;
export default hot(module)(App);

Просмотреть файл

@ -1,6 +1,5 @@
import React from 'react';
import { render } from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'font-awesome/css/font-awesome.css';
@ -11,14 +10,4 @@ import '../css/treeherder-job-buttons.css';
import App from './App';
const load = () => render((
<AppContainer>
<App />
</AppContainer>
), document.getElementById('root'));
if (module.hot) {
module.hot.accept('./App', load);
}
load();
render(<App />, document.getElementById('root'));

10
webpack.config.js Normal file
Просмотреть файл

@ -0,0 +1,10 @@
const neutrino = require('neutrino');
module.exports = (env = {}) => {
// Convert `--env.NAME <value>` CLI arguments into environment variables.
// This makes it possible to write cross-platform-compatible package.json `scripts`.
Object.entries(env).forEach(([name, value]) => {
process.env[name] = value;
});
return neutrino().webpack();
};

5785
yarn.lock

Разница между файлами не показана из-за своего большого размера Загрузить разницу