chore: Upgrade Webpack to v5 (#16701)

* Upgrade Webpack to v5

* Remove mapbox hack

* Replace url-loaders and file-loaders with asset modules

* Remove 1 rule

* Change --colors to --color

* Remove invalid option (--no-progress)

* Remove url-loader, bump plugin

* Fix AnnotationLayer formula check

* Remove redundant tests

* Bump cypress packages

* Remove old comment

* Fix tests

* Remove checks for number of scripts in markdown test

* Cosmetic changes

* Add tests

* Fix test

* Fix test

* Fixes test warnings

* disable flaky test

Co-authored-by: Ville Brofeldt <ville.v.brofeldt@gmail.com>
Co-authored-by: Michael S. Molina <michael.s.molina@gmail.com>
This commit is contained in:
Kamil Gabryjelski 2021-09-22 13:24:54 +02:00 committed by GitHub
parent 9b17e86b44
commit 486e0d412c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 9046 additions and 29792 deletions

View File

@ -70,7 +70,7 @@ build-assets() {
cd "$GITHUB_WORKSPACE/superset-frontend"
say "::group::Build static assets"
npm run build -- --no-progress
npm run build
say "::endgroup::"
}
@ -82,7 +82,7 @@ build-instrumented-assets() {
if [[ -f "$ASSETS_MANIFEST" ]]; then
echo 'Skip frontend build because instrumented static assets already exist.'
else
npm run build-instrumented -- --no-progress
npm run build-instrumented
cache-save instrumented-assets
fi
say "::endgroup::"

View File

@ -24,11 +24,7 @@ describe('Dashboard edit markdown', () => {
cy.visit(TABBED_DASHBOARD);
});
it('should load AceEditor on demand', () => {
let numScripts = 0;
cy.get('script').then(nodes => {
numScripts = nodes.length;
});
it('should add markdown component to dashboard', () => {
cy.get('[data-test="dashboard-header"]')
.find('[aria-label="edit-alt"]')
.click();
@ -37,11 +33,6 @@ describe('Dashboard edit markdown', () => {
cy.get('[data-test="dashboard-header"]')
.find('[aria-label="more-horiz"]')
.click();
cy.get('script').then(nodes => {
// load 5 new script chunks for css editor
expect(nodes.length).to.greaterThan(numScripts);
numScripts = nodes.length;
});
cy.get('[data-test="grid-row-background--transparent"]')
.first()
.as('component-background-first');
@ -49,11 +40,6 @@ describe('Dashboard edit markdown', () => {
drag('[data-test="new-component"]', 'Markdown').to(
'@component-background-first',
);
cy.get('script').then(nodes => {
// load more scripts for markdown editor
expect(nodes.length).to.greaterThan(numScripts);
numScripts = nodes.length;
});
cy.get('[data-test="dashboard-markdown-editor"]')
.should(
'have.text',
@ -75,12 +61,6 @@ describe('Dashboard edit markdown', () => {
cy.get('[data-test="dashboard-markdown-editor"]').contains('Test resize');
// entering edit mode does not add new scripts
// (though scripts may still be removed by others)
cy.get('script').then(nodes => {
expect(nodes.length).to.greaterThan(numScripts);
});
cy.get('@component-background-first').click('right');
cy.get('[data-test="dashboard-component-chart-holder"]')
.find('.ace_content')

View File

@ -26,10 +26,6 @@ describe('AdhocFilters', () => {
cy.verifySliceSuccess({ waitAlias: '@postJson' });
});
xit('Should not load mathjs when not needed', () => {
cy.get('script[src*="mathjs"]').should('have.length', 0);
});
let numScripts = 0;
xit('Should load AceEditor scripts when needed', () => {

View File

@ -105,9 +105,6 @@ describe('VizType control', () => {
cy.get('[role="button"]').contains('Line Chart').click();
cy.get('button').contains('Select').click();
// should load mathjs for line chart
cy.get('script[src*="mathjs"]').should('have.length', 1);
cy.get('button[data-test="run-query-button"]').click();
cy.verifySliceSuccess({
waitAlias: '@lineChartData',

View File

@ -32,14 +32,6 @@ describe('Visualization > Line', () => {
cy.get('.ant-alert-warning').contains(`Metrics: cannot be empty`);
});
it('should preload mathjs', () => {
cy.get('script[src*="mathjs"]').should('have.length', 1);
cy.contains('Add annotation layer').scrollIntoView().click();
// should not load additional mathjs
cy.get('script[src*="mathjs"]').should('have.length', 1);
cy.contains('Layer configuration');
});
it('should not show validator error when metric added', () => {
const formData = { ...LINE_CHART_DEFAULTS, metrics: [] };
cy.visitChartByParams(JSON.stringify(formData));
@ -85,7 +77,6 @@ describe('Visualization > Line', () => {
const formData = { ...LINE_CHART_DEFAULTS, metrics: [NUM_METRIC] };
cy.visitChartByParams(JSON.stringify(formData));
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
cy.get('script[src*="mathjs"]').should('have.length', 1);
});
it('should work with groupby', () => {

File diff suppressed because it is too large Load Diff

View File

@ -7,18 +7,18 @@
"cypress-run-chrome": "cypress run --browser chrome --headless",
"cypress-debug": "cypress open --config watchForFileChanges=true"
},
"author": "Apcahe",
"author": "Apache",
"license": "Apache-2.0",
"dependencies": {
"@cypress/code-coverage": "^3.9.2",
"@superset-ui/core": "^0.17.42",
"@cypress/code-coverage": "^3.9.11",
"@superset-ui/core": "^0.18.4",
"react-dom": "^16.13.0",
"rison": "^0.1.1",
"shortid": "^2.2.15"
},
"devDependencies": {
"cypress": "^6.3.0",
"eslint-plugin-cypress": "^2.11.2"
"eslint-plugin-cypress": "^2.12.1"
},
"nyc": {
"reporter": [

File diff suppressed because it is too large Load Diff

View File

@ -12,12 +12,12 @@
"test": "cross-env NODE_ENV=test jest",
"type": "tsc --noEmit",
"cover": "cross-env NODE_ENV=test jest --coverage",
"dev": "webpack --mode=development --colors --debug --watch",
"dev": "webpack --mode=development --color --watch",
"dev-server": "cross-env NODE_ENV=development BABEL_ENV=development node --max_old_space_size=4096 ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --mode=development",
"prod": "npm run build",
"build-dev": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=development webpack --mode=development --colors",
"build-instrumented": "cross-env NODE_ENV=development BABEL_ENV=instrumented webpack --mode=development --colors",
"build": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=production BABEL_ENV=\"${BABEL_ENV:=production}\" webpack --mode=production --colors",
"build-dev": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=development webpack --mode=development --color",
"build-instrumented": "cross-env NODE_ENV=development BABEL_ENV=instrumented webpack --mode=development --color",
"build": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=production BABEL_ENV=\"${BABEL_ENV:=production}\" webpack --mode=production --color",
"lint": "eslint --ignore-path=.eslintignore --ext .js,.jsx,.ts,.tsx . && npm run type",
"prettier-check": "prettier --check '{src,stylesheets}/**/*.{css,less,sass,scss}'",
"lint-fix": "eslint --fix --ignore-path=.eslintignore --ext .js,.jsx,.ts,tsx . && npm run clean-css && npm run type",
@ -117,7 +117,7 @@
"fuse.js": "^6.4.6",
"geolib": "^2.0.24",
"global-box": "^1.2.0",
"html-webpack-plugin": "^4.5.1",
"html-webpack-plugin": "^5.3.2",
"immer": "^9.0.6",
"immutable": "^4.0.0-rc.12",
"interweave": "^11.2.0",
@ -129,7 +129,7 @@
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"match-sorter": "^6.1.0",
"mathjs": "^8.0.1",
"math-expression-evaluator": "^1.3.8",
"memoize-one": "^5.1.1",
"moment": "^2.26.0",
"moment-timezone": "^0.5.33",
@ -210,7 +210,7 @@
"@storybook/client-api": "^6.1.17",
"@storybook/preset-typescript": "^3.0.0",
"@storybook/react": "^6.1.20",
"@svgr/webpack": "^5.4.0",
"@svgr/webpack": "^5.5.0",
"@testing-library/dom": "^7.29.4",
"@testing-library/jest-dom": "^5.11.6",
"@testing-library/react": "^11.2.0",
@ -252,20 +252,19 @@
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^2.1.3",
"babel-plugin-lodash": "^3.3.4",
"cache-loader": "^1.2.2",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^6.0.3",
"copy-webpack-plugin": "^9.0.1",
"cross-env": "^5.2.0",
"css-loader": "^1.0.0",
"css-loader": "^6.2.0",
"css-minimizer-webpack-plugin": "^3.0.2",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.14.0",
"eslint": "^7.17.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-prettier": "^7.1.0",
"eslint-import-resolver-typescript": "^2.3.0",
"eslint-import-resolver-webpack": "^0.13.0",
"eslint-import-resolver-typescript": "^2.5.0",
"eslint-import-resolver-webpack": "^0.13.1",
"eslint-plugin-cypress": "^2.11.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-jest": "^24.1.3",
"eslint-plugin-jest-dom": "^3.6.5",
"eslint-plugin-jsx-a11y": "^6.4.1",
@ -277,9 +276,9 @@
"exports-loader": "^0.7.0",
"fetch-mock": "^7.7.3",
"file-loader": "^6.0.0",
"fork-ts-checker-webpack-plugin": "^0.4.9",
"fork-ts-checker-webpack-plugin": "^6.3.3",
"ignore-styles": "^5.0.1",
"imports-loader": "^0.7.1",
"imports-loader": "^3.0.0",
"jest": "^26.6.3",
"jest-environment-enzyme": "^7.1.2",
"jest-enzyme": "^7.1.2",
@ -287,32 +286,31 @@
"jsdom": "^16.4.0",
"less": "^3.12.2",
"less-loader": "^5.0.0",
"mini-css-extract-plugin": "^0.4.0",
"mini-css-extract-plugin": "^2.3.0",
"mock-socket": "^9.0.3",
"node-fetch": "^2.6.1",
"optimize-css-assets-webpack-plugin": "^5.0.1",
"po2json": "^0.4.5",
"prettier": "^2.2.1",
"process": "^0.11.10",
"react-test-renderer": "^16.9.0",
"redux-mock-store": "^1.5.4",
"sinon": "^9.0.2",
"source-map-support": "^0.5.16",
"speed-measure-webpack-plugin": "^1.2.3",
"speed-measure-webpack-plugin": "^1.5.0",
"storybook-addon-jsx": "^7.3.3",
"storybook-addon-paddings": "^3.2.0",
"style-loader": "^1.0.0",
"thread-loader": "^1.2.0",
"transform-loader": "^0.2.3",
"style-loader": "^3.2.1",
"thread-loader": "^3.0.4",
"transform-loader": "^0.2.4",
"ts-jest": "^26.4.2",
"ts-loader": "^8.0.7",
"ts-loader": "^9.2.5",
"typescript": "^4.1.6",
"url-loader": "^1.0.1",
"webpack": "^4.42.0",
"webpack-bundle-analyzer": "^3.6.1",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.11.0",
"webpack-manifest-plugin": "^2.2.0",
"webpack-sources": "^1.4.3",
"webpack": "^5.52.1",
"webpack-bundle-analyzer": "^4.4.2",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.2.0",
"webpack-manifest-plugin": "^4.0.2",
"webpack-sources": "^3.2.0",
"yargs": "^15.4.1"
},
"stylelint": {

View File

@ -20,7 +20,7 @@
import React, { useEffect, useRef, useState } from 'react';
import AntdIcon from '@ant-design/icons';
import { styled } from '@superset-ui/core';
import { ReactComponent as TransparentIcon } from 'images/icons/transparent.svg';
import TransparentIcon from 'images/icons/transparent.svg';
import IconType from './IconType';
const AntdIconComponent = ({

View File

@ -20,7 +20,7 @@ import { t, styled } from '@superset-ui/core';
import React, { useEffect } from 'react';
import { Empty } from 'src/common/components';
import Alert from 'src/components/Alert';
import { ReactComponent as EmptyImage } from 'images/empty.svg';
import EmptyImage from 'images/empty.svg';
import cx from 'classnames';
import Button from 'src/components/Button';
import Icons from 'src/components/Icons';

View File

@ -20,7 +20,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { CompactPicker } from 'react-color';
import Button from 'src/components/Button';
import { parse as mathjsParse } from 'mathjs';
import mexp from 'math-expression-evaluator';
import {
t,
SupersetClient,
@ -204,15 +204,30 @@ export default class AnnotationLayer extends React.PureComponent {
return sources;
}
isValidFormula(value, annotationType) {
isValidFormula(expression, annotationType) {
if (annotationType === ANNOTATION_TYPES.FORMULA) {
try {
mathjsParse(value).compile().evaluate({ x: 0 });
const token = {
type: 3,
token: 'x',
show: 'x',
value: 'x',
};
// handle input like "y = x+1" instead of just "x+1"
const subexpressions = expression.split('=');
if (
subexpressions.length > 2 ||
(subexpressions[1] && !subexpressions[0].match(/^\s*[a-zA-Z]\w*\s*$/))
) {
return false;
}
mexp.eval(subexpressions[1] ?? subexpressions[0], [token], { x: 0 });
} catch (err) {
return true;
return false;
}
}
return false;
return true;
}
isValidForm() {
@ -238,7 +253,7 @@ export default class AnnotationLayer extends React.PureComponent {
errors.push(validateNonEmpty(intervalEndColumn));
}
}
errors.push(this.isValidFormula(value, annotationType));
errors.push(!this.isValidFormula(value, annotationType));
return !errors.filter(x => x).length;
}
@ -442,7 +457,7 @@ export default class AnnotationLayer extends React.PureComponent {
value={value}
onChange={this.handleValue}
validationErrors={
this.isValidFormula(value, annotationType) ? ['Bad formula.'] : []
!this.isValidFormula(value, annotationType) ? ['Bad formula.'] : []
}
/>
);

View File

@ -57,16 +57,18 @@ beforeAll(() => {
);
});
test('renders with default props', () => {
const { container } = render(<AnnotationLayer {...defaultProps} />);
expect(container).toBeInTheDocument();
const waitForRender = (props?: any) =>
waitFor(() => render(<AnnotationLayer {...defaultProps} {...props} />));
test('renders with default props', async () => {
await waitForRender();
expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'OK' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Cancel' })).toBeEnabled();
});
test('renders extra checkboxes when type is time series', () => {
render(<AnnotationLayer {...defaultProps} />);
test('renders extra checkboxes when type is time series', async () => {
await waitForRender();
expect(
screen.queryByRole('button', { name: 'Show Markers' }),
).not.toBeInTheDocument();
@ -82,7 +84,7 @@ test('renders extra checkboxes when type is time series', () => {
});
test('enables apply and ok buttons', async () => {
render(<AnnotationLayer {...defaultProps} />);
await waitForRender();
userEvent.type(screen.getByLabelText('Name'), 'Test');
userEvent.type(screen.getByLabelText('Formula'), '2x');
await waitFor(() => {
@ -91,68 +93,47 @@ test('enables apply and ok buttons', async () => {
});
});
test('triggers addAnnotationLayer when apply button is clicked', () => {
test('triggers addAnnotationLayer when apply button is clicked', async () => {
const addAnnotationLayer = jest.fn();
render(
<AnnotationLayer
{...defaultProps}
name="Test"
value="2x"
addAnnotationLayer={addAnnotationLayer}
/>,
);
await waitForRender({ name: 'Test', value: '2x', addAnnotationLayer });
userEvent.click(screen.getByRole('button', { name: 'Apply' }));
expect(addAnnotationLayer).toHaveBeenCalled();
});
test('triggers addAnnotationLayer and close when ok button is clicked', () => {
test('triggers addAnnotationLayer and close when ok button is clicked', async () => {
const addAnnotationLayer = jest.fn();
const close = jest.fn();
render(
<AnnotationLayer
{...defaultProps}
name="Test"
value="2x"
addAnnotationLayer={addAnnotationLayer}
close={close}
/>,
);
await waitForRender({ name: 'Test', value: '2x', addAnnotationLayer, close });
userEvent.click(screen.getByRole('button', { name: 'OK' }));
expect(addAnnotationLayer).toHaveBeenCalled();
expect(close).toHaveBeenCalled();
});
test('triggers close when cancel button is clicked', () => {
test('triggers close when cancel button is clicked', async () => {
const close = jest.fn();
render(<AnnotationLayer {...defaultProps} close={close} />);
await waitForRender({ close });
userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
expect(close).toHaveBeenCalled();
});
test('triggers removeAnnotationLayer and close when remove button is clicked', () => {
test('triggers removeAnnotationLayer and close when remove button is clicked', async () => {
const removeAnnotationLayer = jest.fn();
const close = jest.fn();
render(
<AnnotationLayer
{...defaultProps}
name="Test"
value="2x"
removeAnnotationLayer={removeAnnotationLayer}
close={close}
/>,
);
await waitForRender({
name: 'Test',
value: '2x',
removeAnnotationLayer,
close,
});
userEvent.click(screen.getByRole('button', { name: 'Remove' }));
expect(removeAnnotationLayer).toHaveBeenCalled();
expect(close).toHaveBeenCalled();
});
test('renders chart options', async () => {
render(
<AnnotationLayer
{...defaultProps}
annotationType={ANNOTATION_TYPES_METADATA.EVENT.value}
/>,
);
await waitForRender({
annotationType: ANNOTATION_TYPES_METADATA.EVENT.value,
});
userEvent.click(screen.getByText('2 option(s)'));
userEvent.click(screen.getByText('Superset annotation'));
expect(await screen.findByLabelText('Annotation layer')).toBeInTheDocument();
@ -162,15 +143,12 @@ test('renders chart options', async () => {
});
test('keeps apply disabled when missing required fields', async () => {
render(
<AnnotationLayer
{...defaultProps}
annotationType={ANNOTATION_TYPES_METADATA.EVENT.value}
sourceType="Table"
/>,
);
userEvent.click(await screen.findByText('1 option(s)'));
userEvent.click(screen.getByText('Chart A'));
await waitForRender({
annotationType: ANNOTATION_TYPES_METADATA.EVENT.value,
sourceType: 'Table',
});
userEvent.click(screen.getByText('1 option(s)'));
await waitFor(() => userEvent.click(screen.getByText('Chart A')));
expect(
screen.getByText('Annotation Slice Configuration'),
).toBeInTheDocument();
@ -188,3 +166,48 @@ test('keeps apply disabled when missing required fields', async () => {
expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled();
});
test.skip('Disable apply button if formula is incorrect', async () => {
// TODO: fix flaky test that passes locally but fails on CI
await waitForRender({ name: 'test' });
userEvent.clear(screen.getByLabelText('Formula'));
userEvent.type(screen.getByLabelText('Formula'), 'x+1');
await waitFor(() => {
expect(screen.getByLabelText('Formula')).toHaveValue('x+1');
expect(screen.getByRole('button', { name: 'OK' })).toBeEnabled();
expect(screen.getByRole('button', { name: 'Apply' })).toBeEnabled();
});
userEvent.clear(screen.getByLabelText('Formula'));
userEvent.type(screen.getByLabelText('Formula'), 'y = x*2+1');
await waitFor(() => {
expect(screen.getByLabelText('Formula')).toHaveValue('y = x*2+1');
expect(screen.getByRole('button', { name: 'OK' })).toBeEnabled();
expect(screen.getByRole('button', { name: 'Apply' })).toBeEnabled();
});
userEvent.clear(screen.getByLabelText('Formula'));
userEvent.type(screen.getByLabelText('Formula'), 'y+1');
await waitFor(() => {
expect(screen.getByLabelText('Formula')).toHaveValue('y+1');
expect(screen.getByRole('button', { name: 'OK' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled();
});
userEvent.clear(screen.getByLabelText('Formula'));
userEvent.type(screen.getByLabelText('Formula'), 'x+');
await waitFor(() => {
expect(screen.getByLabelText('Formula')).toHaveValue('x+');
expect(screen.getByRole('button', { name: 'OK' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled();
});
userEvent.clear(screen.getByLabelText('Formula'));
userEvent.type(screen.getByLabelText('Formula'), 'y = z+1');
await waitFor(() => {
expect(screen.getByLabelText('Formula')).toHaveValue('y = z+1');
expect(screen.getByRole('button', { name: 'OK' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled();
});
});

View File

@ -37,7 +37,7 @@ export default class FilterBoxChartPlugin extends ChartPlugin {
controlPanel,
metadata,
transformProps,
loadChart: () => import('./FilterBox.jsx'),
loadChart: () => import('./FilterBox'),
});
}
}

View File

@ -44,7 +44,7 @@ export default class TimeTableChartPlugin extends ChartPlugin {
super({
metadata,
transformProps,
loadChart: () => import('./TimeTable.jsx'),
loadChart: () => import('./TimeTable'),
});
}
}

View File

@ -21,17 +21,19 @@ const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');
const {
WebpackManifestPlugin,
getCompilerHooks,
} = require('webpack-manifest-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const parsedArgs = require('yargs').argv;
const getProxyConfig = require('./webpack.proxy-config');
const packageConfig = require('./package.json');
const packageConfig = require('./package');
// input dir
const APP_DIR = path.resolve(__dirname, './');
@ -56,8 +58,8 @@ const output = {
publicPath: `${ASSET_BASE_URL}/static/assets/`,
};
if (isDevMode) {
output.filename = '[name].[hash:8].entry.js';
output.chunkFilename = '[name].[hash:8].chunk.js';
output.filename = '[name].[contenthash:8].entry.js';
output.chunkFilename = '[name].[contenthash:8].chunk.js';
} else if (nameChunks) {
output.filename = '[name].[chunkhash].entry.js';
output.chunkFilename = '[name].[chunkhash].chunk.js';
@ -66,9 +68,17 @@ if (isDevMode) {
output.chunkFilename = '[chunkhash].chunk.js';
}
if (!isDevMode) {
output.clean = true;
}
const plugins = [
new webpack.ProvidePlugin({
process: 'process/browser',
}),
// creates a manifest.json mapping of name to hashed output used in template files
new ManifestPlugin({
new WebpackManifestPlugin({
publicPath: output.publicPath,
seed: { app: 'superset' },
// This enables us to include all relevant files for an entry
@ -109,9 +119,10 @@ const plugins = [
// runs type checking on a separate process to speed up the build
new ForkTsCheckerWebpackPlugin({
eslint: true,
checkSyntacticErrors: true,
memoryLimit: 4096,
eslint: {
files: './src/**/*.{ts,tsx,js,jsx}',
memoryLimit: 4096,
},
}),
new CopyPlugin({
@ -141,17 +152,6 @@ if (!process.env.CI) {
plugins.push(new webpack.ProgressPlugin());
}
// clean up built assets if not from dev-server
if (!isDevServer) {
plugins.push(
new CleanWebpackPlugin({
dry: false,
// required because the build directory is outside the frontend directory:
dangerouslyAllowCleanPatternsOutsideProject: true,
}),
);
}
if (!isDevMode) {
// text loading (webpack 4+)
plugins.push(
@ -160,7 +160,6 @@ if (!isDevMode) {
chunkFilename: '[name].[chunkhash].chunk.css',
}),
);
plugins.push(new OptimizeCSSAssetsPlugin());
}
const PREAMBLE = [path.join(APP_DIR, '/src/preamble.ts')];
@ -199,9 +198,6 @@ const babelLoader = {
};
const config = {
node: {
fs: 'empty',
},
entry: {
preamble: PREAMBLE,
theme: path.join(APP_DIR, '/src/theme.ts'),
@ -268,13 +264,6 @@ const config = {
].join('|')})/`,
),
},
// bundle large libraries separately
mathjs: {
name: 'mathjs',
test: /\/node_modules\/mathjs\//,
priority: 30,
enforce: true,
},
// viz thumbnails are used in `addSlice` and `explore` page
thumbnail: {
name: 'thumbnail',
@ -284,6 +273,7 @@ const config = {
},
},
},
minimizer: [new CssMinimizerPlugin()],
},
resolve: {
modules: [APP_DIR, 'node_modules', ROOT_DIR],
@ -302,16 +292,21 @@ const config = {
},
extensions: ['.ts', '.tsx', '.js', '.jsx', '.yml'],
symlinks: false,
fallback: {
fs: false,
vm: false,
path: false,
},
},
context: APP_DIR, // to automatically find tsconfig.json
module: {
// Uglifying mapbox-gl results in undefined errors, see
// https://github.com/mapbox/mapbox-gl-js/issues/4359#issuecomment-288001933
noParse: /(mapbox-gl)\.js$/,
rules: [
{
test: /datatables\.net.*/,
loader: 'imports-loader?define=>false',
loader: 'imports-loader',
options: {
additionalCode: 'var define = false;',
},
},
{
test: /\.tsx?$/,
@ -388,52 +383,34 @@ const config = {
{
test: /\.png$/,
issuer: {
exclude: /\/src\/assets\/staticPages\//,
not: [/\/src\/assets\/staticPages\//],
},
loader: 'url-loader',
options: {
limit: 10000,
name: '[name].[hash:8].[ext]',
type: 'asset',
generator: {
filename: '[name].[contenthash:8].[ext]',
},
},
{
test: /\.png$/,
issuer: {
test: /\/src\/assets\/staticPages\//,
},
loader: 'url-loader',
options: {
limit: 150000, // Convert images < 150kb to base64 strings
},
issuer: /\/src\/assets\/staticPages\//,
type: 'asset',
},
{
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
issuer: {
test: /\.(j|t)sx?$/,
},
issuer: /\.([jt])sx?$/,
use: ['@svgr/webpack'],
},
{
test: /\.(jpg|gif)$/,
loader: 'file-loader',
options: {
name: '[name].[hash:8].[ext]',
type: 'asset/resource',
generator: {
filename: '[name].[contenthash:8].[ext]',
},
},
/* for font-awesome */
{
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
loader: 'url-loader?limit=10000&mimetype=application/font-woff',
options: {
esModule: false,
},
},
{
test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
loader: 'file-loader',
options: {
esModule: false,
},
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
{
test: /\.ya?ml$/,
@ -456,20 +433,15 @@ let proxyConfig = getProxyConfig();
if (isDevMode) {
config.devtool = 'eval-cheap-module-source-map';
config.devServer = {
before(app, server, compiler) {
onBeforeSetupMiddleware(devServer) {
// load proxy config when manifest updates
const hook = compiler.hooks.webpackManifestPluginAfterEmit;
hook.tap('ManifestPlugin', manifest => {
const { afterEmit } = getCompilerHooks(devServer.compiler);
afterEmit.tap('ManifestPlugin', manifest => {
proxyConfig = getProxyConfig(manifest);
});
},
historyApiFallback: true,
hot: true,
injectClient: false,
injectHot: true,
inline: true,
stats: 'minimal',
overlay: true,
port: devserverPort,
// Only serves bundled files from webpack-dev-server
// and proxy everything else to Superset backend
@ -477,7 +449,11 @@ if (isDevMode) {
// functions are called for every request
() => proxyConfig,
],
contentBase: path.join(process.cwd(), '../static/assets'),
client: {
overlay: { errors: true, warnings: false },
logging: 'error',
},
static: path.join(process.cwd(), '../static/assets'),
};
// make sure to use @emotion/* modules in the root directory