From 92404918a07bed3ac26f89923e3a5a55e9ab3d5f Mon Sep 17 00:00:00 2001 From: John Furrow Date: Fri, 22 Nov 2019 22:47:09 -0800 Subject: [PATCH] Introduce Typescript & CSS Modules (#815) * Adding typescript support * Begins configuring webpack builds * Fix lint warnings * Updates react-router * Fixes lint configuration * Adds missing dependency * Restores disabled performance hints * Renames connectStores * Types connectStores * Uses correct envvars and fixes missing EOF newline * Formats files * Defaults props to empty object * Ignores type definitions in eslint * Another newline * Adjusts script invocation * Ignore jsdoc output * Undoes the autoformatting of CSS module types * Improves lint rules * Finishes webpack config changes * Updates deps * Fixes lint errors and attempts to fix SVG icon generator * Fixes SVG sprite generator * Adds type for SVG imports * Explicitly use babelrc in SVG loader * Formats files * Refactors prettier formatter, formats CSS module type defs * Updates style types * Uses nicer syntax in typed-css-modules-loader * Removes unnecessary div * optional property in package.json * package-lock * Fixes upstream lint errors * Removes unused modules --- .babelrc | 11 +- .eslintignore | 2 + .travis.yml | 1 + client/.eslintrc.js | 21 +- client/config/paths.js | 2 +- client/config/webpack.config.dev.js | 304 +- client/config/webpack.config.prod.js | 317 +- client/scripts/build.js | 2 +- client/scripts/start.js | 2 +- .../scripts/svg-react-component-generator.js | 56 + client/scripts/typed-css-modules-loader.js | 28 + client/src/javascript/actions/FloodActions.js | 4 +- client/src/javascript/{app.js => app.tsx} | 70 +- .../javascript/components/auth/AuthForm.js | 8 +- .../components/general/CustomScrollbars.js | 6 +- .../javascript/components/general/Icon.scss | 17 + .../components/general/Icon.scss.d.ts | 7 + .../javascript/components/general/Icon.tsx | 47 + .../components/modals/feeds-modal/FeedsTab.js | 2 +- .../modals/settings-modal/DiskUsageTab.js | 4 +- .../torrent-details-modal/TorrentHeading.js | 4 +- .../components/sidebar/TransferRateDetails.js | 2 +- .../src/javascript/constants/ActionTypes.js | 2 +- client/src/javascript/constants/EventTypes.js | 2 +- .../i18n/{languages.js => languages.ts} | 0 .../javascript/stores/TorrentFilterStore.js | 2 +- client/src/javascript/stores/TorrentStore.js | 2 +- .../javascript/stores/TransferDataStore.js | 2 +- client/src/javascript/util/connectStores.js | 61 - client/src/javascript/util/connectStores.tsx | 112 + client/src/javascript/util/filterTorrents.js | 2 +- client/src/javascript/util/history.js | 8 + .../javascript/util/torrentStatusClasses.js | 2 +- .../src/javascript/util/torrentStatusIcons.js | 2 +- client/src/javascript/util/validators.js | 2 +- client/src/sass/style.scss.d.ts | 527 ++++ custom.d.ts | 4 + package-lock.json | 2621 ++++++++++++----- package.json | 47 +- scripts/prettier.js | 143 +- shared/util/stringUtil.js | 2 + tsconfig.json | 19 + 42 files changed, 3251 insertions(+), 1228 deletions(-) create mode 100644 client/scripts/svg-react-component-generator.js create mode 100644 client/scripts/typed-css-modules-loader.js rename client/src/javascript/{app.js => app.tsx} (62%) create mode 100644 client/src/javascript/components/general/Icon.scss create mode 100644 client/src/javascript/components/general/Icon.scss.d.ts create mode 100644 client/src/javascript/components/general/Icon.tsx rename client/src/javascript/i18n/{languages.js => languages.ts} (100%) delete mode 100644 client/src/javascript/util/connectStores.js create mode 100644 client/src/javascript/util/connectStores.tsx create mode 100644 client/src/javascript/util/history.js create mode 100644 client/src/sass/style.scss.d.ts create mode 100644 custom.d.ts create mode 100644 tsconfig.json diff --git a/.babelrc b/.babelrc index 58fb2089..6858a5f6 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,11 @@ { - "presets": ["@babel/env", "@babel/react"], - "plugins": ["@babel/plugin-proposal-class-properties", "@babel/proposal-object-rest-spread"] + "presets": [ + "@babel/env", + "@babel/typescript", + "@babel/react" + ], + "plugins": [ + "@babel/plugin-proposal-class-properties", + "@babel/proposal-object-rest-spread" + ] } diff --git a/.eslintignore b/.eslintignore index ebe70da2..ee52212e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,4 @@ /node_modules /server/assets +**/*.d.ts +/docs diff --git a/.travis.yml b/.travis.yml index b0d248ec..bdda7644 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,5 +13,6 @@ before_script: script: - npm run check-source-formatting - npm run lint + - npm run check-types - npm run test - npm run build diff --git a/client/.eslintrc.js b/client/.eslintrc.js index d54ee5f0..91f1aa3e 100644 --- a/client/.eslintrc.js +++ b/client/.eslintrc.js @@ -1,6 +1,8 @@ const path = require('path'); module.exports = { + parser: 'babel-eslint', + extends: ['plugin:@typescript-eslint/recommended', 'prettier', 'prettier/@typescript-eslint'], env: { browser: 1, node: 0, @@ -12,6 +14,12 @@ module.exports = { }, plugins: ['import'], rules: { + '@typescript-eslint/no-var-requires': 0, + // This is enabled to allow BEM-style classnames to be referenced JS + // Remvoe when BEM-style classnames are converted to locally-scoped + // class names + '@typescript-eslint/camelcase': ['error', {allow: [/[^\s]__[^\s]{1,}$/]}], + camelcase: 0, // TODO: Enable a11y features 'jsx-a11y/click-events-have-key-events': 0, 'jsx-a11y/label-has-associated-control': 0, @@ -25,7 +33,7 @@ module.exports = { 'react/destructuring-assignment': 0, 'react/forbid-prop-types': 0, 'react/jsx-closing-bracket-location': 0, - 'react/jsx-filename-extension': [1, {extensions: ['.js']}], + 'react/jsx-filename-extension': [1, {extensions: ['.js', '.tsx']}], 'react/jsx-one-expression-per-line': 0, 'react/jsx-wrap-multilines': 0, 'react/no-unescaped-entities': ['error', {forbid: ['>', '}']}], @@ -50,4 +58,15 @@ module.exports = { }, }, }, + overrides: [ + { + files: ['*.ts', '*.tsx', '**/*.ts', '**/*.tsx'], + parser: '@typescript-eslint/parser', + plugins: ['import', '@typescript-eslint/eslint-plugin'], + rules: { + 'no-unused-vars': 0, + '@typescript-eslint/no-unused-vars': 1, + }, + }, + ], }; diff --git a/client/config/paths.js b/client/config/paths.js index 2bc54ce1..856f2cec 100644 --- a/client/config/paths.js +++ b/client/config/paths.js @@ -21,7 +21,7 @@ module.exports = { appBuild: resolveApp('server/assets'), appPublic: resolveApp('client/src/public/'), appHtml: resolveApp('client/src/index.html'), - appIndexJs: resolveApp('client/src/javascript/app.js'), + appIndex: resolveApp('client/src/javascript/app.tsx'), appPackageJson: resolveApp('package.json'), appSrc: resolveApp('./'), clientSrc: resolveApp('client/src'), diff --git a/client/config/webpack.config.dev.js b/client/config/webpack.config.dev.js index 8404e515..f0c7082b 100644 --- a/client/config/webpack.config.dev.js +++ b/client/config/webpack.config.dev.js @@ -9,116 +9,128 @@ const eslintFormatter = require('react-dev-utils/eslintFormatter'); const getClientEnvironment = require('./env'); const paths = require('./paths'); -// Webpack uses `publicPath` to determine where the app is being served from. -// In development, we always serve from the root. This makes config easier. -const publicPath = '/'; -// Get environment variables to inject into our app. const env = getClientEnvironment(); -// This is the development configuration. -// It is focused on developer experience and fast rebuilds. -// The production configuration is different and lives in a separate file. module.exports = { mode: process.env.NODE_ENV, - // You may want 'eval' instead if you prefer to see the compiled output in DevTools. - // See the discussion in https://github.com/facebookincubator/create-react-app/issues/343. - devtool: 'cheap-module-source-map', - // These are the "entry points" to our application. - // This means they will be the "root" imports that are included in JS bundle. - // The first two entry points enable "hot" CSS and auto-refreshes for JS. - entry: [ - // Include an alternative client for WebpackDevServer. A client's job is to - // connect to WebpackDevServer by a socket and get notified about changes. - // When you save a file, the client will either apply hot updates (in case - // of CSS changes), or refresh the page (in case of JS changes). When you - // make a syntax error, this client will display a syntax error overlay. - // Note: instead of the default WebpackDevServer client, we use a custom one - // to bring better experience for Create React App users. You can replace - // the line below with these two lines if you prefer the stock client: - // require.resolve('webpack-dev-server/client') + '?/', - // require.resolve('webpack/hot/dev-server'), - require.resolve('react-dev-utils/webpackHotDevClient'), - // We ship a few polyfills by default: - require.resolve('./polyfills'), - // Finally, this is your app's code: - paths.appIndexJs, - // We include the app code last so that if there is a runtime error during - // initialization, it doesn't blow up the WebpackDevServer client, and - // changing JS code would still trigger a refresh. - ], - output: { - // Next line is not used in dev but WebpackDevServer crashes without it: - path: paths.appBuild, - // Add /* filename */ comments to generated require()s in the output. - pathinfo: true, - // This does not produce a real file. It's just the virtual path that is - // served by WebpackDevServer in development. This is the JS bundle - // containing code from all our entry points, and the Webpack runtime. - filename: 'static/js/bundle.js', - // There are also additional JS chunk files if you use code splitting. - chunkFilename: 'static/js/[name].chunk.js', - // This is the URL that app is served from. We use "/" in development. - publicPath, - // Point sourcemap entries to original disk location (format as URL on Windows) - devtoolModuleFilenameTemplate: info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'), - }, - resolve: { - // This allows you to set a fallback for where Webpack should look for modules. - // We placed these paths second because we want `node_modules` to "win" - // if there are any conflicts. This matches Node resolution mechanism. - // https://github.com/facebookincubator/create-react-app/issues/253 - modules: ['node_modules', paths.appNodeModules].concat( - // It is guaranteed to exist because we tweak it in `env.js` - process.env.NODE_PATH.split(path.delimiter).filter(Boolean), - ), - // These are the reasonable defaults supported by the Node ecosystem. - // We also include JSX as a common component filename extension to support - // some tools, although we do not recommend using it, see: - // https://github.com/facebookincubator/create-react-app/issues/290 - // `web` extension prefixes have been added for better support - // for React Native Web. - extensions: ['.web.js', '.js', '.json', '.web.jsx', '.jsx'], - alias: { - // Support React Native Web - // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/ - 'react-native': 'react-native-web', - 'universally-shared-code': path.resolve('./shared'), - }, - }, module: { - strictExportPresence: true, rules: [ - // TODO: Disable require.ensure as it's not a standard language feature. - // We are waiting for https://github.com/facebookincubator/create-react-app/issues/2176. - // { parser: { requireEnsure: false } }, - - // First, run the linter. - // It's important to do this before Babel processes the JS. { - test: /\.(js|jsx)$/, enforce: 'pre', + test: /\.(ts|js)x?$/, + exclude: /node_modules/, use: [ { + loader: 'eslint-loader', options: { - emitWarning: true, formatter: eslintFormatter, + emitWarning: true, }, - loader: require.resolve('eslint-loader'), }, ], - include: paths.clientSrc, }, - // ** ADDING/UPDATING LOADERS ** - // The "file" loader handles all assets unless explicitly excluded. - // The `exclude` list *must* be updated with every change to loader extensions. - // When adding a new loader, you must add its `test` - // as a new entry in the `exclude` list for "file" loader. - - // "file" loader makes sure those assets get served by WebpackDevServer. - // When you `import` an asset, you get its (virtual) filename. - // In production, they would get copied to the `build` folder. { - exclude: [/\.html$/, /\.(js|jsx)$/, /\.css$/, /\.scss$/, /\.json$/, /\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], + test: /\.(ts|js)x?$/, + exclude: /node_modules/, + use: [ + { + loader: 'babel-loader', + options: { + babelrc: true, + }, + }, + ], + }, + { + test: /\.s?css$/, + use: [ + { + loader: 'style-loader', + }, + { + loader: 'css-loader', + options: { + importLoaders: 1, + sourceMap: true, + modules: { + mode: 'global', + localIdentName: '[name]_[local]__[hash:base64:5]', + }, + }, + }, + { + loader: require.resolve('../scripts/typed-css-modules-loader'), + }, + { + loader: 'postcss-loader', + options: { + sourceMap: true, + // Necessary for external CSS imports to work + // https://github.com/facebookincubator/create-react-app/issues/2677 + ident: 'postcss', + plugins: () => [ + autoprefixer({ + browsers: ['>1%'], + }), + ], + }, + }, + { + loader: 'sass-loader', + options: { + sourceMap: true, + }, + }, + ], + }, + { + test: /\.(ts|js)x?$/, + use: ['source-map-loader'], + enforce: 'pre', + }, + { + enforce: 'pre', + test: /\.svg$/, + issuer: /\.(ts|js)x?$/, + use: [ + { + loader: 'babel-loader', + options: { + babelrc: true, + }, + }, + { + loader: 'svg-sprite-loader', + options: { + runtimeGenerator: require.resolve('../scripts/svg-react-component-generator'), + runtimeOptions: { + iconModule: require.resolve('../src/javascript/components/general/Icon.tsx'), + }, + }, + }, + ], + }, + { + exclude: [ + /\.html$/, + /\.(js|jsx|ts|tsx)$/, + /\.css$/, + /\.scss$/, + /\.json$/, + /\.bmp$/, + /\.gif$/, + /\.jpe?g$/, + /\.png$/, + /\.svg$/, + ], + loader: require.resolve('file-loader'), + options: { + name: 'static/media/[name].[hash:8].[ext]', + }, + }, + { + include: [/\.svg$/], + issuer: /\.s?css$/, loader: require.resolve('file-loader'), options: { name: 'static/media/[name].[hash:8].[ext]', @@ -135,68 +147,33 @@ module.exports = { name: 'static/media/[name].[hash:8].[ext]', }, }, - // Process JS with Babel. - { - test: /\.(js|jsx)$/, - exclude: /node_modules/, - include: paths.appSrc, - loader: require.resolve('babel-loader'), - options: { - // This is a feature of `babel-loader` for webpack (not Babel itself). - // It enables caching results in ./node_modules/.cache/babel-loader/ - // directory for faster rebuilds. - cacheDirectory: true, - }, - }, - // "postcss" loader applies autoprefixer to our CSS. - // "css" loader resolves paths in CSS and adds assets as dependencies. - // "style" loader turns CSS into JS modules that inject