[SIP-9] Introduce TypeScript (#6120)

* [SIP-9] Introduce TypeScript

- Introduce TypeScript and co to both source and tests
- Define alias for src directory in both webpack config and jest config so we can avoid using long relative paths like ../../src in both source and tests
- Type check feature flags system to prevent typos of flag names
- Change the feature flags system and the flags on window instead of populating them through the state tree. When introducing the first SCOPED_FILTER feature flag, it became too difficult to pipe the flags through the state initializers and layers of components and containers (the resulting code is hard to read and has a handful of methods taking an additional feature flag map parameter). Given that feature flags don't change throughout the life time of the app, it is better to leave them on window for easy access than piping them through the global state tree, which is meant to store the state of the app which changes frequently.
- Add a barebone filter panel that only shows when the SCOPED_FILTER feature flag is on

* Remove unnecessary dev-dependency on gl

* - Adding linting for TypeScript files via tslint.
- Fixing linting for Javascript files importing Typscript files
- Also fix linting for Javascript files that now leverage the webpack alias for the src directory

- up Typescript and type def versions

* Rename src directory's webpack alias from @ to src to be more explicit.
This commit is contained in:
Christine Chambers 2018-10-23 22:33:51 -07:00 committed by Maxime Beauchemin
parent 9e6b171ee9
commit 5f1eaa49f2
31 changed files with 1129 additions and 1131 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@
*.pyc
*.sqllite
*.swp
.cache-loader
.coverage
.DS_Store
.eggs

View File

@ -292,6 +292,21 @@ npm run dev-server -- --supersetPort=8081
After adding or upgrading an NPM package by changing `package.json`, you must run `yarn install`, which will regenerate the `yarn.lock` file. Then, be sure to commit the new `yarn.lock` so that other users' builds are reproducible. See [the Yarn docs](https://yarnpkg.com/blog/2016/11/24/lockfiles-for-all/) for more information.
#### Feature flags
Superset supports a server-wide feature flag system, which eases the incremental development of features. To add a new feature flag, simply modify `superset_config.py` with something like the following:
```
FEATURE_FLAGS = {
'SCOPED_FILTER': True,
}
```
If you want to use the same flag in the client code, also add it to the FeatureFlag TypeScript enum in `superset/assets/src/featureFlags.ts`. For example,
```
export enum FeatureFlag {
SCOPED_FILTER = 'SCOPED_FILTER',
}
```
## Testing
All tests are carried out in [tox](http://tox.readthedocs.io/en/latest/index.html)

View File

@ -18,7 +18,6 @@
"func-names": 0,
"react/jsx-no-bind": 0,
"no-confusing-arrow": 0,
"jsx-a11y/no-static-element-interactions": 0,
"jsx-a11y/anchor-has-content": 0,
"react/require-default-props": 0,
@ -40,6 +39,10 @@
"react/no-string-refs": 0,
"indent": 0,
"no-multi-spaces": 0,
"padded-blocks": 0
"padded-blocks": 0,
"import/extensions": [".js", ".jsx", ".ts", ".tsx", ".json"]
},
"settings": {
"import/resolver": "webpack"
}
}

View File

@ -1,11 +1,17 @@
module.exports = {
testRegex: '\\/spec\\/.*_spec\\.jsx?$',
testRegex: '\\/spec\\/.*_spec\\.(j|t)sx?$',
moduleNameMapper: {
'\\.(css|less)$': '<rootDir>/spec/__mocks__/styleMock.js',
'\\.(gif|ttf|eot|svg)$': '<rootDir>/spec/__mocks__/fileMock.js',
'^src/(.*)$': '<rootDir>/src/$1',
},
setupTestFrameworkScriptFile: '<rootDir>/spec/helpers/shim.js',
testURL: 'http://localhost',
collectCoverageFrom: ['src/**/*.{js,jsx}'],
collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'],
coverageDirectory: '<rootDir>/coverage/',
transform: {
'^.+\\.jsx?$': 'babel-jest',
'^.+\\.tsx?$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
};

View File

@ -15,8 +15,8 @@
"dev-server": "webpack-dev-server --mode=development --progress",
"prod": "node --max_old_space_size=4096 webpack --mode=production --colors --progress",
"build": "NODE_ENV=production webpack --mode=production --colors --progress",
"lint": "eslint --ignore-path=.eslintignore --ext .js,.jsx .",
"lint-fix": "eslint --fix --ignore-path=.eslintignore --ext .js,.jsx .",
"lint": "eslint --ignore-path=.eslintignore --ext .js,.jsx . && tslint -c tslint.json ./src/**/*.ts{,x}",
"lint-fix": "eslint --fix --ignore-path=.eslintignore --ext .js,.jsx . && tslint -c tslint.json --fix ./src/**/*.ts{,x}",
"sync-backend": "babel-node --presets env src/syncBackend.js",
"cypress": "cypress",
"cypress-debug": "cypress open --config watchForFileChanges=true"
@ -131,6 +131,9 @@
"viewport-mercator-project": "^5.0.0"
},
"devDependencies": {
"@types/jest": "^23.3.5",
"@types/react": "^16.4.18",
"@types/react-dom": "^16.0.9",
"babel-cli": "^6.14.0",
"babel-core": "^6.10.4",
"babel-eslint": "^8.2.2",
@ -143,6 +146,7 @@
"babel-polyfill": "^6.23.0",
"babel-preset-airbnb": "^2.1.1",
"babel-preset-env": "^1.7.0",
"cache-loader": "^1.2.2",
"clean-webpack-plugin": "^0.1.19",
"css-loader": "^1.0.0",
"cypress": "^3.0.3",
@ -151,6 +155,7 @@
"eslint": "^4.19.0",
"eslint-config-airbnb": "^15.0.1",
"eslint-config-prettier": "^2.9.0",
"eslint-import-resolver-webpack": "^0.10.1",
"eslint-plugin-cypress": "^2.0.1",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jest": "^21.24.1",
@ -161,7 +166,7 @@
"exports-loader": "^0.7.0",
"fetch-mock": "^7.0.0-alpha.6",
"file-loader": "^1.1.11",
"gl": "^4.0.4",
"fork-ts-checker-webpack-plugin": "^0.4.9",
"ignore-styles": "^5.0.1",
"imports-loader": "^0.7.1",
"jest": "^23.6.0",
@ -181,7 +186,13 @@
"speed-measure-webpack-plugin": "^1.2.3",
"style-loader": "^0.21.0",
"terser-webpack-plugin": "^1.1.0",
"thread-loader": "^1.2.0",
"transform-loader": "^0.2.3",
"ts-jest": "^23.10.4",
"ts-loader": "^5.2.0",
"tslint": "^5.11.0",
"tslint-react": "^3.6.0",
"typescript": "^3.1.3",
"url-loader": "^1.0.1",
"webpack": "^4.19.0",
"webpack-assets-manifest": "^3.0.1",

View File

@ -1,38 +0,0 @@
import React from 'react';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { shallow } from 'enzyme';
import Dashboard from '../../../../src/dashboard/containers/Dashboard';
import getInitialState from '../../../../src/dashboard/reducers/getInitialState';
describe('Dashboard Container', () => {
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
let store;
let wrapper;
beforeAll(() => {
const bootstrapData = {
dashboard_data: {
slices: [],
metadata: {},
},
common: {
feature_flags: {
FOO_BAR: true,
},
conf: {},
},
};
store = mockStore(getInitialState(bootstrapData), {});
});
beforeEach(() => {
wrapper = shallow(<Dashboard />, { context: { store } });
});
it('should set feature flags', () => {
expect(wrapper.prop('isFeatureEnabled')('FOO_BAR')).toBe(true);
});
});

View File

@ -1,11 +1,9 @@
import React from 'react';
import { shallow } from 'enzyme';
import { getFormDataFromControls, defaultControls }
from '../../../../src/explore/store';
import {
ControlPanelsContainer,
} from '../../../../src/explore/components/ControlPanelsContainer';
import ControlPanelSection from '../../../../src/explore/components/ControlPanelSection';
import { getFormDataFromControls, defaultControls } from 'src/explore/store';
import { ControlPanelsContainer } from 'src/explore/components/ControlPanelsContainer';
import ControlPanelSection from 'src/explore/components/ControlPanelSection';
import * as featureFlags from 'src/featureFlags';
const defaultProps = {
datasource_type: 'table',
@ -18,12 +16,22 @@ const defaultProps = {
describe('ControlPanelsContainer', () => {
let wrapper;
let scopedFilterOn = false;
const isFeatureEnabledMock = jest.spyOn(featureFlags, 'isFeatureEnabled')
.mockImplementation(() => scopedFilterOn);
beforeEach(() => {
wrapper = shallow(<ControlPanelsContainer {...defaultProps} />);
afterAll(() => {
isFeatureEnabledMock.mockRestore();
});
it('renders ControlPanelSections', () => {
wrapper = shallow(<ControlPanelsContainer {...defaultProps} />);
expect(wrapper.find(ControlPanelSection)).toHaveLength(6);
});
it('renders filter panel when SCOPED_FILTER flag is on', () => {
scopedFilterOn = true;
wrapper = shallow(<ControlPanelsContainer {...defaultProps} />);
expect(wrapper.find(ControlPanelSection)).toHaveLength(7);
});
});

View File

@ -3,40 +3,39 @@ import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { shallow } from 'enzyme';
import getInitialState from '../../../../src/explore/reducers/getInitialState';
import ExploreViewContainer
from '../../../../src/explore/components/ExploreViewContainer';
import QueryAndSaveBtns
from '../../../../src/explore/components/QueryAndSaveBtns';
import ControlPanelsContainer
from '../../../../src/explore/components/ControlPanelsContainer';
import ChartContainer
from '../../../../src/explore/components/ExploreChartPanel';
import getInitialState from 'src/explore/reducers/getInitialState';
import ExploreViewContainer from 'src/explore/components/ExploreViewContainer';
import QueryAndSaveBtns from 'src/explore/components/QueryAndSaveBtns';
import ControlPanelsContainer from 'src/explore/components/ControlPanelsContainer';
import ChartContainer from 'src/explore/components/ExploreChartPanel';
import * as featureFlags from 'src/featureFlags';
describe('ExploreViewContainer', () => {
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
let store;
let wrapper;
let isFeatureEnabledMock;
beforeAll(() => {
isFeatureEnabledMock = jest.spyOn(featureFlags, 'isFeatureEnabled')
.mockReturnValue(false);
const bootstrapData = {
common: {
feature_flags: {
FOO_BAR: true,
},
conf: {},
},
datasource: {
columns: [],
},
form_data: {
datasource: {},
},
};
store = mockStore(getInitialState(bootstrapData), {});
});
afterAll(() => {
isFeatureEnabledMock.mockRestore();
});
beforeEach(() => {
wrapper = shallow(<ExploreViewContainer />, {
context: { store },
@ -44,10 +43,6 @@ describe('ExploreViewContainer', () => {
});
});
it('should set feature flags', () => {
expect(wrapper.prop('isFeatureEnabled')('FOO_BAR')).toBe(true);
});
it('renders', () => {
expect(
React.isValidElement(<ExploreViewContainer />),

View File

@ -5,35 +5,20 @@ import thunk from 'redux-thunk';
import { shallow } from 'enzyme';
import sinon from 'sinon';
import App from '../../../src/SqlLab/components/App';
import TabbedSqlEditors from '../../../src/SqlLab/components/TabbedSqlEditors';
import getInitialState from '../../../src/SqlLab/getInitialState';
import App from 'src/SqlLab/components/App';
import TabbedSqlEditors from 'src/SqlLab/components/TabbedSqlEditors';
import { sqlLabReducer } from 'src/SqlLab/reducers';
describe('SqlLab App', () => {
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
let store;
const store = mockStore(sqlLabReducer(undefined, {}), {});
let wrapper;
beforeAll(() => {
const bootstrapData = {
common: {
feature_flags: {
FOO_BAR: true,
},
},
};
store = mockStore(getInitialState(bootstrapData), {});
});
beforeEach(() => {
wrapper = shallow(<App />, { context: { store } });
});
it('should set feature flags', () => {
expect(wrapper.prop('isFeatureEnabled')('FOO_BAR')).toBe(true);
});
it('is valid', () => {
expect(React.isValidElement(<App />)).toBe(true);
});

View File

@ -4,6 +4,7 @@ import { Provider } from 'react-redux';
import thunkMiddleware from 'redux-thunk';
import { hot } from 'react-hot-loader';
import { initFeatureFlags } from 'src/featureFlags';
import getInitialState from './getInitialState';
import rootReducer from './reducers';
import { initEnhancer } from '../reduxUtils';
@ -18,6 +19,7 @@ appSetup();
const appContainer = document.getElementById('app');
const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
initFeatureFlags(bootstrapData.common.feature_flags);
const state = getInitialState(bootstrapData);
const store = createStore(

View File

@ -9,7 +9,6 @@ import QueryAutoRefresh from './QueryAutoRefresh';
import QuerySearch from './QuerySearch';
import ToastPresenter from '../../messageToasts/containers/ToastPresenter';
import * as Actions from '../actions';
import { isFeatureEnabledCreator } from '../../featureFlags';
class App extends React.PureComponent {
constructor(props) {
@ -84,10 +83,6 @@ App.propTypes = {
actions: PropTypes.object,
};
const mapStateToProps = state => ({
isFeatureEnabled: isFeatureEnabledCreator(state),
});
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Actions, dispatch),
@ -96,6 +91,6 @@ function mapDispatchToProps(dispatch) {
export { App };
export default connect(
mapStateToProps,
null,
mapDispatchToProps,
)(App);

View File

@ -14,7 +14,6 @@ export default function getInitialState({ defaultDbId, ...restBootstrapData }) {
};
return {
featureFlags: restBootstrapData.common.feature_flags,
sqlLab: {
activeSouthPaneTab: 'Results',
alerts: [],

View File

@ -13,7 +13,6 @@ import {
getFromArr,
addToArr,
} from '../reduxUtils';
import featureFlags from '../featureFlags';
import { t } from '../locales';
export const sqlLabReducer = function (state = {}, action) {
@ -280,7 +279,6 @@ export const sqlLabReducer = function (state = {}, action) {
};
export default combineReducers({
featureFlags,
sqlLab: sqlLabReducer,
messageToasts,
});

View File

@ -4,6 +4,7 @@ import { createStore, applyMiddleware, compose } from 'redux';
import { Provider } from 'react-redux';
import { hot } from 'react-hot-loader';
import { initFeatureFlags } from 'src/featureFlags';
import { initEnhancer } from '../reduxUtils';
import { appSetup } from '../common';
import DashboardContainer from './containers/Dashboard';
@ -14,6 +15,7 @@ appSetup();
const appContainer = document.getElementById('app');
const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
initFeatureFlags(bootstrapData.common.feature_flags);
const initState = getInitialState(bootstrapData);
const store = createStore(

View File

@ -1,7 +1,6 @@
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { isFeatureEnabledCreator } from '../../featureFlags';
import Dashboard from '../components/Dashboard';
import {
@ -23,7 +22,6 @@ function mapStateToProps(state) {
} = state;
return {
isFeatureEnabled: isFeatureEnabledCreator(state),
initMessages: dashboardInfo.common.flash_messages,
timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
userId: dashboardInfo.userId,

View File

@ -138,7 +138,6 @@ export default function(bootstrapData) {
};
return {
featureFlags: common.feature_flags,
datasources,
sliceEntities: { ...initSliceEntities, slices, isLoading: false },
charts: chartQueries,

View File

@ -5,14 +5,12 @@ import dashboardState from './dashboardState';
import datasources from './datasources';
import sliceEntities from './sliceEntities';
import dashboardLayout from '../reducers/undoableDashboardLayout';
import featureFlags from '../../featureFlags';
import messageToasts from '../../messageToasts/reducers';
const dashboardInfo = (state = {}) => state;
const impressionId = (state = '') => state;
export default combineReducers({
featureFlags,
charts,
datasources,
dashboardInfo,

View File

@ -4,6 +4,7 @@ import { createStore, applyMiddleware, compose } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import { initFeatureFlags } from 'src/featureFlags';
import { initEnhancer } from '../reduxUtils';
import ToastPresenter from '../messageToasts/containers/ToastPresenter';
import ExploreViewContainer from './components/ExploreViewContainer';
@ -18,6 +19,7 @@ appSetup();
const exploreViewContainer = document.getElementById('app');
const bootstrapData = JSON.parse(exploreViewContainer.getAttribute('data-bootstrap'));
initFeatureFlags(bootstrapData.common.feature_flags);
const initState = getInitialState(bootstrapData);
const store = createStore(

View File

@ -15,7 +15,6 @@ import { chartPropShape } from '../../dashboard/util/propShapes';
import * as exploreActions from '../actions/exploreActions';
import * as saveModalActions from '../actions/saveModalActions';
import * as chartActions from '../../chart/chartAction';
import { isFeatureEnabledCreator } from '../../featureFlags';
import { Logger, ActionLog, EXPLORE_EVENT_NAMES, LOG_ACTIONS_MOUNT_EXPLORER } from '../../logger';
const propTypes = {
@ -308,7 +307,6 @@ function mapStateToProps(state) {
const chartKey = Object.keys(charts)[0];
const chart = charts[chartKey];
return {
isFeatureEnabled: isFeatureEnabledCreator(state),
isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
datasource: explore.datasource,
datasource_type: explore.datasource.type,

View File

@ -0,0 +1,9 @@
import * as React from 'react';
export default function FilterPanel() {
return (
<div>
test filter
</div>
);
}

View File

@ -19,6 +19,7 @@ import ViewportControl from './ViewportControl';
import VizTypeControl from './VizTypeControl';
import MetricsControl from './MetricsControl';
import AdhocFilterControl from './AdhocFilterControl';
import FilterPanel from './FilterPanel';
const controlMap = {
AnnotationLayerControl,
@ -42,5 +43,6 @@ const controlMap = {
VizTypeControl,
MetricsControl,
AdhocFilterControl,
FilterPanel,
};
export default controlMap;

View File

@ -1880,6 +1880,10 @@ export const controls = {
provideFormDataToProps: true,
},
filters: {
type: 'FilterPanel',
},
slice_id: {
type: 'HiddenControl',
label: t('Chart ID'),

View File

@ -31,7 +31,6 @@ export default function getInitialState(bootstrapData) {
const chartKey = getChartKey(bootstrappedState);
return {
featureFlags: bootstrapData.common.feature_flags,
charts: {
[chartKey]: {
id: chartKey,

View File

@ -3,13 +3,11 @@ import { combineReducers } from 'redux';
import charts from '../../chart/chartReducer';
import saveModal from './saveModalReducer';
import explore from './exploreReducer';
import featureFlags from '../../featureFlags';
import messageToasts from '../../messageToasts/reducers';
const impressionId = (state = '') => state;
export default combineReducers({
featureFlags,
charts,
saveModal,
explore,

View File

@ -3,6 +3,7 @@
* and associated with each and every visualization type.
*/
import React from 'react';
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import { D3_TIME_FORMAT_OPTIONS } from './controls';
import * as v from './validators';
import { t } from '../locales';
@ -41,6 +42,13 @@ export const sections = {
['time_range'],
],
},
filters: {
label: t('Filters'),
expanded: true,
controlSetRows: [
['filters'],
],
},
annotations: {
label: t('Annotations and Layers'),
expanded: true,
@ -1919,6 +1927,7 @@ export function sectionsToRender(vizType, datasourceType) {
return [].concat(
sectionsCopy.datasourceAndVizType,
datasourceType === 'table' ? sectionsCopy.sqlaTimeSeries : sectionsCopy.druidTimeSeries,
isFeatureEnabled(FeatureFlag.SCOPED_FILTER) ? sectionsCopy.filters : undefined,
viz.controlPanelSections,
).filter(section => section);
}

View File

@ -1,11 +0,0 @@
// A higher-order function that takes the redux state tree and returns a
// `isFeatureEnabled` function which takes a feature and returns whether it is enabled.
// Note that we assume the featureFlags subtree is at the root of the redux state tree.
export function isFeatureEnabledCreator(state) {
return feature => !!state.featureFlags[feature];
}
// Feature flags are not altered throughout the life time of the app
export default function featureFlagsReducer(state = {}) {
return state;
}

View File

@ -0,0 +1,23 @@
// We can codegen the enum definition based on a list of supported flags that we
// check into source control. We're hardcoding the supported flags for now.
export enum FeatureFlag {
SCOPED_FILTER = 'SCOPED_FILTER',
}
export type FeatureFlagMap = {
[key in FeatureFlag]?: boolean;
};
declare global {
interface Window {
featureFlags: FeatureFlagMap;
}
}
export function initFeatureFlags(featureFlags: FeatureFlagMap) {
window.featureFlags = featureFlags || {};
}
export function isFeatureEnabled(feature: FeatureFlag) {
return !!window.featureFlags[feature];
}

View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"baseUrl": ".",
"outDir": "./dist",
"module": "commonjs",
"target": "es5",
"lib": ["es6", "dom"],
"sourceMap": true,
"allowJs": true,
"jsx": "react",
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"importHelpers": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": true,
"skipLibCheck": true
},
"include": ["./src/**/*", "./spec/**/*"]
}

View File

@ -0,0 +1,10 @@
{
"extends": ["tslint:recommended", "tslint-react"],
"jsRules": {
},
"rules": {
"interface-name" : [true, "never-prefix"],
"quotemark": [true, "single"]
},
"rulesDirectory": []
}

View File

@ -1,3 +1,4 @@
const os = require('os');
const path = require('path');
const webpack = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
@ -7,6 +8,7 @@ const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const WebpackAssetsManifest = require('webpack-assets-manifest');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
// Parse command-line arguments
const parsedArgs = require('minimist')(process.argv.slice(2));
@ -45,6 +47,11 @@ const plugins = [
new webpack.DefinePlugin({
'process.env.WEBPACK_MODE': JSON.stringify(mode),
}),
// runs type checking on a separate process to speed up the build
new ForkTsCheckerWebpackPlugin({
checkSyntacticErrors: true,
}),
];
if (isDevMode) {
@ -102,8 +109,12 @@ const config = {
},
},
resolve: {
extensions: ['.js', '.jsx'],
alias: {
src: path.resolve(APP_DIR, './src'),
},
extensions: ['.ts', '.tsx', '.js', '.jsx'],
},
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
@ -113,6 +124,27 @@ const config = {
test: /datatables\.net.*/,
loader: 'imports-loader?define=>false',
},
{
test: /\.tsx?$/,
use: [
{ loader: 'cache-loader' },
{
loader: 'thread-loader',
options: {
// there should be 1 cpu for the fork-ts-checker-webpack-plugin
workers: os.cpus().length - 1,
},
},
{
loader: 'ts-loader',
options: {
// transpile only in happyPack mode
// type checking is done via fork-ts-checker-webpack-plugin
happyPackMode: true,
},
},
],
},
{
test: /\.jsx?$/,
exclude: /node_modules/,

File diff suppressed because it is too large Load Diff