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
This commit is contained in:
John Furrow
2019-11-22 22:47:09 -08:00
committed by GitHub
parent 459ed84ca0
commit 92404918a0
42 changed files with 3251 additions and 1228 deletions

View File

@@ -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"
]
}

View File

@@ -1,2 +1,4 @@
/node_modules
/server/assets
**/*.d.ts
/docs

View File

@@ -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

View File

@@ -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,
},
},
],
};

View File

@@ -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'),

View File

@@ -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 <style> tags.
// In production, we use a plugin to extract that CSS to a file, but
// in development "style" loader enables hot editing of CSS.
{
test: /\.scss$/,
use: [
require.resolve('style-loader'),
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
sourceMap: true,
},
},
{
loader: require.resolve('postcss-loader'),
options: {
// Necessary for external CSS imports to work
// https://github.com/facebookincubator/create-react-app/issues/2677
ident: 'postcss',
plugins: () => [
require('postcss-flexbugs-fixes'),
autoprefixer({
browsers: [
'>1%',
'last 4 versions',
'Firefox ESR',
'not ie < 9', // React doesn't support IE8 anyway
],
flexbox: 'no-2009',
}),
],
sourceMap: true,
},
},
{
loader: require.resolve('sass-loader'),
options: {
sourceMap: true,
},
},
],
},
// ** STOP ** Are you adding a new loader?
// Remember to add the new extension(s) to the "file" loader exclusion list.
],
},
entry: paths.appIndex,
resolve: {
extensions: ['*', '.js', '.jsx', '.ts', '.tsx', '.json'],
alias: {
'@shared': path.resolve('./shared'),
},
},
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.
// Webpack uses `publicPath` to determine where the app is being served from.
// In development, we always serve from the root. This makes config easier.
publicPath: '/',
// Point sourcemap entries to original disk location (format as URL on Windows)
devtoolModuleFilenameTemplate: info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'),
},
plugins: [
// Makes some environment variables available in index.html.
// The base URI is available as %BASE_URI% in index.html, e.g.:
@@ -224,26 +201,5 @@ module.exports = {
// makes the discovery automatic so you don't have to restart.
// See https://github.com/facebookincubator/create-react-app/issues/186
new WatchMissingNodeModulesPlugin(paths.appNodeModules),
// TODO: Come back to this... pretty sure we need all of moment's locales.
// Moment.js is an extremely popular library that bundles large locale files
// by default due to how Webpack interprets its code. This is a practical
// solution that requires the user to opt into importing specific locales.
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
// You can remove this if you don't use Moment.js:
// new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
],
// Some libraries import Node modules but don't use them in the browser.
// Tell Webpack to provide empty mocks for them so importing them works.
node: {
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
},
// Turn off performance hints during development because we don't do any
// splitting or minification in interest of speed. These warnings become
// cumbersome.
performance: {
hints: false,
},
};

View File

@@ -2,19 +2,11 @@ const autoprefixer = require('autoprefixer');
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
const eslintFormatter = require('react-dev-utils/eslintFormatter');
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
const TerserPlugin = require('terser-webpack-plugin');
const paths = require('./paths');
const ManifestPlugin = require('webpack-manifest-plugin');
const getClientEnvironment = require('./env');
const paths = require('./paths');
// Webpack uses `publicPath` to determine where the app is being served from.
// It requires a trailing slash, or the file assets will get an incorrect path.
const publicPath = paths.servedPath;
// Get environment variables to inject into our app.
const env = getClientEnvironment();
// Assert this just to be safe.
@@ -23,110 +15,119 @@ if (env.stringified['process.env'].NODE_ENV !== '"production"') {
throw new Error('Production builds must have NODE_ENV=production.');
}
// Note: defined here because it will be used more than once.
const cssFilename = 'static/css/[name].[hash:8].css';
// ExtractTextPlugin expects the build output to be flat.
// (See https://github.com/webpack-contrib/extract-text-webpack-plugin/issues/27)
// However, our output is structured with css, js and media folders.
// To have this structure working with relative paths, we have to use custom options.
const extractTextPluginOptions = {};
// This is the production configuration.
// It compiles slowly and is focused on producing a fast and minimal bundle.
// The development configuration is different and lives in a separate file.
module.exports = {
mode: process.env.NODE_ENV,
// Don't attempt to continue if there are any errors.
bail: true,
// We generate sourcemaps in production. This is slow but gives good results.
// You can exclude the *.map files from the build during deployment.
devtool: 'source-map',
// In production, we only want to load the polyfills and the app code.
entry: [require.resolve('./polyfills'), paths.appIndexJs],
output: {
// The build folder.
path: paths.appBuild,
// Generated JS file names (with nested folders).
// There will be one main bundle, and one file per asynchronous chunk.
// We don't currently advertise code splitting but Webpack supports it.
filename: 'static/js/[name].[chunkhash:8].js',
chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js',
// We inferred the "public path" (such as / or /my-project) from homepage.
publicPath,
// Point sourcemap entries to original disk location (format as URL on Windows)
devtoolModuleFilenameTemplate: info => path.relative(paths.appSrc, 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'),
},
plugins: [
// Prevents users from importing files from outside of src/ (or node_modules/).
// This often causes confusion because we only process files within src/ with babel.
// To fix this, we prevent you from importing files out of src/ -- if you'd like to,
// please link the files into your node_modules/ and let module-resolution kick in.
// Make sure your source files are compiled, as they will not be processed in any way.
new ModuleScopePlugin(paths.appSrc),
],
},
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: 'babel-loader',
options: {
formatter: eslintFormatter,
babelrc: 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 in the "file" loader.
// "file" loader makes sure those assets end up in the `build` folder.
// When you `import` an asset, you get its filename.
{
exclude: [/\.html$/, /\.(js|jsx)$/, /\.scss$/, /\.css$/, /\.json$/, /\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
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',
},
{
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]',
},
},
// "url" loader works just like "file" loader but it also embeds
// assets smaller than specified size as data URLs to avoid requests.
{
include: [/\.svg$/],
issuer: /\.s?css$/,
loader: require.resolve('file-loader'),
options: {
name: 'static/media/[name].[hash:8].[ext]',
},
},
// "url" loader works like "file" loader except that it embeds assets
// smaller than specified limit in bytes as data URLs to avoid requests.
// A missing `test` is equivalent to a match.
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
loader: require.resolve('url-loader'),
@@ -135,93 +136,34 @@ 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: {
compact: true,
},
},
// The notation here is somewhat confusing.
// "postcss" loader applies autoprefixer to our CSS.
// "css" loader resolves paths in CSS and adds assets as dependencies.
// "style" loader normally turns CSS into JS modules injecting <style>,
// but unlike in development configuration, we do something different.
// `ExtractTextPlugin` first applies the "postcss" and "css" loaders
// (second argument), then grabs the result CSS and puts it into a
// separate file in our build process. This way we actually ship
// a single CSS file in production instead of JS code injecting <style>
// tags. If you use code splitting, however, any async bundles will still
// use the "style" loader inside the async code so CSS from them won't be
// in the main CSS file.
{
test: /\.scss$/,
loader: ExtractTextPlugin.extract(
Object.assign(
{
fallback: require.resolve('style-loader'),
use: [
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
sourceMap: true,
},
},
{
loader: require.resolve('postcss-loader'),
options: {
// Necessary for external CSS imports to work
// https://github.com/facebookincubator/create-react-app/issues/2677
ident: 'postcss',
plugins: () => [
require('postcss-flexbugs-fixes'),
autoprefixer({
browsers: [
'>1%',
'last 4 versions',
'Firefox ESR',
'not ie < 9', // React doesn't support IE8 anyway
],
flexbox: 'no-2009',
}),
],
},
},
{
loader: require.resolve('sass-loader'),
},
],
},
extractTextPluginOptions,
),
),
// Note: this won't work without `new ExtractTextPlugin()` in `plugins`.
},
// ** STOP ** Are you adding a new loader?
// Remember to add the new extension(s) to the "file" loader exclusion list.
],
},
optimization: {
minimizer: [
new TerserPlugin({
cache: true,
parallel: true,
sourceMap: true,
}),
],
entry: paths.appIndex,
resolve: {
extensions: ['*', '.js', '.jsx', '.ts', '.tsx', '.json'],
alias: {
'@shared': path.resolve('./shared'),
},
},
performance: {
// TODO: Add code-splitting and re-enable this when the bundle is smaller
hints: false,
output: {
// The build folder.
path: paths.appBuild,
// Generated JS file names (with nested folders).
// There will be one main bundle, and one file per asynchronous chunk.
// We don't currently advertise code splitting but Webpack supports it.
filename: 'static/js/[name].[chunkhash:8].js',
chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js',
// Webpack uses `publicPath` to determine where the app is being served from.
// It requires a trailing slash, or the file assets will get an incorrect path.
publicPath: paths.servedPath,
// Point sourcemap entries to original disk location (format as URL on Windows)
devtoolModuleFilenameTemplate: info => path.relative(paths.appSrc, info.absoluteResourcePath).replace(/\\/g, '/'),
},
plugins: [
// Makes some environment variables available in index.html.
// The base URI is available as %BASE_URI% in index.html, e.g.:
// <link rel="shortcut icon" href="%BASE_URI%/favicon.ico">
// In development, this will be an empty string.
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin({
@@ -240,35 +182,16 @@ module.exports = {
minifyURLs: true,
},
}),
// Makes some environment variables available to the JS code, for example:
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
// It is absolutely essential that NODE_ENV was set to production here.
// Otherwise React will be compiled in the very slow development mode.
new webpack.DefinePlugin(env.stringified),
// Note: this won't work without ExtractTextPlugin.extract(..) in `loaders`.
new ExtractTextPlugin({
filename: cssFilename,
}),
// Generate a manifest file which contains a mapping of all asset filenames
// to their corresponding output file so that tools can pick it up without
// having to parse `index.html`.
new ManifestPlugin({
fileName: 'asset-manifest.json',
}),
// TODO: Come back to this... pretty sure we need all of moment's locales.
// Moment.js is an extremely popular library that bundles large locale files
// by default due to how Webpack interprets its code. This is a practical
// solution that requires the user to opt into importing specific locales.
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
// You can remove this if you don't use Moment.js:
// new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
],
// Some libraries import Node modules but don't use them in the browser.
// Tell Webpack to provide empty mocks for them so importing them works.
node: {
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
performance: {
// TODO: Add code-splitting and re-enable this when the bundle is smaller
hints: false,
},
};

View File

@@ -28,7 +28,7 @@ const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
// Warn and crash if required files are missing
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
if (!checkRequiredFiles([paths.appHtml, paths.appIndex])) {
process.exit(1);
}

View File

@@ -28,7 +28,7 @@ const userConfig = require('../../config');
const isInteractive = process.stdout.isTTY;
// Warn and crash if required files are missing
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
if (!checkRequiredFiles([paths.appHtml, paths.appIndex])) {
process.exit(1);
}

View File

@@ -0,0 +1,56 @@
const path = require('path');
const pascalCase = require('pascal-case');
const {stringifyRequest} = require('loader-utils');
const stringifiedRegexp = /^'|".*'|"$/;
const stringify = content => {
if (typeof content === 'string' && stringifiedRegexp.test(content)) {
return content;
}
return JSON.stringify(content, null, 2);
};
const stringifySymbol = symbol =>
stringify({
id: symbol.id,
use: symbol.useId,
viewBox: symbol.viewBox,
content: symbol.render(),
});
const runtimeGenerator = ({symbol, config, loaderContext}) => {
const {spriteModule, symbolModule, runtimeOptions} = config;
// eslint-disable-next-line no-underscore-dangle
const compilerContext = loaderContext._compiler.context;
const iconModulePath = path.resolve(compilerContext, runtimeOptions.iconModule);
const iconModuleRequest = stringify(path.relative(path.dirname(symbol.request.file), iconModulePath));
const spriteRequest = stringifyRequest(
{context: loaderContext.context},
path.resolve(loaderContext.context, spriteModule),
);
const symbolRequest = stringifyRequest(
{context: loaderContext.context},
path.resolve(loaderContext.context, symbolModule),
);
const parentComponentDisplayName = 'SpriteSymbolComponent';
const displayName = `${pascalCase(symbol.id)}_${parentComponentDisplayName}`;
return `
import * as React from 'react';
import SpriteSymbol from ${symbolRequest};
import sprite from ${spriteRequest};
import ${parentComponentDisplayName} from ${iconModuleRequest};
const symbol = new SpriteSymbol(${stringifySymbol(symbol)});
sprite.add(symbol);
const ${displayName} = props => {
return <${parentComponentDisplayName} glyph="${symbol.id}" {...props} />;
};
export default ${displayName};
`;
};
module.exports = runtimeGenerator;

View File

@@ -0,0 +1,28 @@
const chalk = require('chalk');
const DtsCreator = require('typed-css-modules');
const prettier = require('../../scripts/prettier');
const creator = new DtsCreator();
module.exports = async function moduleLoader(source, map) {
if (this.cacheable) {
this.cacheable();
}
try {
const callback = this.async();
const dtsContent = await creator.create(this.resourcePath, source);
await dtsContent.writeFile();
await prettier.formatFile(dtsContent.outputFilePath, dtsContent.outputFilePath);
return callback(null, source, map);
} catch (error) {
console.log(chalk.red(chalk.red('CSS module type generation failed.')));
console.log(error.message);
if (error.stack != null) {
console.log(chalk.gray(error.stack));
}
}
};

View File

@@ -1,6 +1,6 @@
import axios from 'axios';
import historySnapshotTypes from 'universally-shared-code/constants/historySnapshotTypes';
import serverEventTypes from 'universally-shared-code/constants/serverEventTypes';
import historySnapshotTypes from '@shared/constants/historySnapshotTypes';
import serverEventTypes from '@shared/constants/serverEventTypes';
import AppDispatcher from '../dispatcher/AppDispatcher';
import ActionTypes from '../constants/ActionTypes';

View File

@@ -1,14 +1,16 @@
import {Router} from 'react-router-dom';
import {FormattedMessage, IntlProvider} from 'react-intl';
import {IndexRoute, Router, Route, browserHistory} from 'react-router';
import {Route} from 'react-router';
import React from 'react';
import ReactDOM from 'react-dom';
import * as i18n from './i18n/languages';
import connectStores from './util/connectStores';
import connectStores, {EventListenerDescriptor} from './util/connectStores';
import AppWrapper from './components/AppWrapper';
import AuthActions from './actions/AuthActions';
import EventTypes from './constants/EventTypes';
import FloodActions from './actions/FloodActions';
import history from './util/history';
import Login from './components/views/Login';
import Register from './components/views/Register';
import SettingsStore from './stores/SettingsStore';
@@ -17,7 +19,7 @@ import UIStore from './stores/UIStore';
import '../sass/style.scss';
const initialize = () => {
const initialize = (): void => {
UIStore.registerDependency({
id: 'notifications',
message: <FormattedMessage id="dependency.loading.notifications" defaultMessage="Notifications" />,
@@ -47,15 +49,15 @@ const initialize = () => {
});
AuthActions.verify().then(
({initialUser}) => {
({initialUser}): void => {
if (initialUser) {
browserHistory.replace('register');
history.replace('register');
} else {
browserHistory.replace('overview');
history.replace('overview');
}
},
() => {
browserHistory.replace('login');
(): void => {
history.replace('login');
},
);
@@ -63,27 +65,28 @@ const initialize = () => {
};
const appRoutes = (
<Router history={browserHistory}>
<Route path="/" component={AppWrapper}>
<IndexRoute component={Login} />
<Route path="login" component={Login} />
<Route path="register" component={Register} />
<Route path="overview" component={TorrentClientOverview} />
<Route path="*" component={Login} />
</Route>
<Router history={history}>
<AppWrapper>
<Route path="/login" component={Login} />
<Route path="/register" component={Register} />
<Route path="/overview" component={TorrentClientOverview} />
</AppWrapper>
</Router>
);
class FloodApp extends React.Component {
componentDidMount() {
interface InjectedFloodAppProps {
locale: keyof typeof i18n;
}
class FloodApp extends React.Component<InjectedFloodAppProps> {
public componentDidMount(): void {
initialize();
}
render() {
public render(): React.ReactNode {
const {locale} = this.props;
return (
// eslint-disable-next-line import/namespace
<IntlProvider locale={locale} messages={i18n[locale]}>
{appRoutes}
</IntlProvider>
@@ -91,18 +94,21 @@ class FloodApp extends React.Component {
}
}
const ConnectedFloodApp = connectStores(FloodApp, () => {
return [
{
store: SettingsStore,
event: EventTypes.SETTINGS_CHANGE,
getValue: ({store}) => {
return {
locale: store.getFloodSettings('language'),
};
const ConnectedFloodApp = connectStores<InjectedFloodAppProps>(
FloodApp,
(): EventListenerDescriptor<InjectedFloodAppProps>[] => {
return [
{
store: SettingsStore,
event: EventTypes.SETTINGS_CHANGE,
getValue: (): InjectedFloodAppProps => {
return {
locale: SettingsStore.getFloodSettings('language'),
};
},
},
},
];
});
];
},
);
ReactDOM.render(<ConnectedFloodApp />, document.getElementById('app'));

View File

@@ -1,4 +1,3 @@
import {browserHistory} from 'react-router';
import {injectIntl} from 'react-intl';
import React from 'react';
@@ -6,6 +5,7 @@ import {Button, Form, FormError, FormRow, Panel, PanelContent, PanelHeader, Pane
import AuthActions from '../../actions/AuthActions';
import AuthStore from '../../stores/AuthStore';
import connectStores from '../../util/connectStores';
import history from '../../util/history';
import EventTypes from '../../constants/EventTypes';
import RtorrentConnectionTypeSelection from '../general/RtorrentConnectionTypeSelection';
@@ -53,10 +53,10 @@ class AuthForm extends React.Component {
password: submission.formData.password,
})
.then(() => {
this.setState({isSubmitting: false}, () => browserHistory.replace('overview'));
this.setState({isSubmitting: false}, () => history.replace('overview'));
})
.catch(() => {
this.setState({isSubmitting: false}, () => browserHistory.replace('login'));
this.setState({isSubmitting: false}, () => history.replace('login'));
});
} else {
AuthActions.register({
@@ -67,7 +67,7 @@ class AuthForm extends React.Component {
socketPath: submission.formData.rtorrentSocketPath,
isAdmin: true,
}).then(() => {
this.setState({isSubmitting: false}, () => browserHistory.replace('overview'));
this.setState({isSubmitting: false}, () => history.replace('overview'));
});
}
};

View File

@@ -52,10 +52,12 @@ export default class CustomScrollbar extends React.Component {
children,
className,
inverted,
getHorizontalThumb,
getVerticalThumb,
nativeScrollHandler,
scrollHandler,
/* eslint-disable @typescript-eslint/no-unused-vars */
getHorizontalThumb: _getHorizontalThumb,
getVerticalThumb: _getVerticalThumb,
/* eslint-enable @typescript-eslint/no-unused-vars */
...otherProps
} = this.props;
const classes = classnames('scrollbars', className, {

View File

@@ -0,0 +1,17 @@
.icon {
display: block;
}
.sizeSmall {
height: 12px;
width: 12px;
}
.sizeMedium {
height: 16px;
width: 16px;
}
.fillCurrentColor {
fill: currentColor;
}

View File

@@ -0,0 +1,7 @@
declare const styles: {
readonly icon: string;
readonly sizeSmall: string;
readonly sizeMedium: string;
readonly fillCurrentColor: string;
};
export = styles;

View File

@@ -0,0 +1,47 @@
import classnames from 'classnames';
import * as React from 'react';
import * as styles from './Icon.scss';
enum Fills {
NONE,
CURRENT_COLOR,
}
enum Sizes {
SMALL,
MEDIUM,
}
interface Props {
className?: string;
fill: Fills;
glyph: string;
size: Sizes;
}
export default class Icon extends React.PureComponent<Props> {
public static Fills = Fills;
public static Sizes = Sizes;
public static defaultProps = {
fill: Fills.CURRENT_COLOR,
size: Sizes.MEDIUM,
};
public render(): React.ReactNode {
const {className, fill, glyph, size, ...restProps} = this.props;
return (
<svg
className={classnames(styles.icon, className, {
[styles.fillCurrentColor]: fill === Fills.CURRENT_COLOR,
[styles.sizeMedium]: size === Sizes.MEDIUM,
[styles.sizeSmall]: size === Sizes.SMALL,
})}
{...restProps}>
<use xlinkHref={`#${glyph}`} />
</svg>
);
}
}

View File

@@ -12,7 +12,7 @@ import {
SelectItem,
Textbox,
} from 'flood-ui-kit';
import formatUtil from 'universally-shared-code/util/formatUtil';
import formatUtil from '@shared/util/formatUtil';
import React from 'react';
import Edit from '../../icons/Edit';

View File

@@ -1,4 +1,4 @@
import {Checkbox, Form, FormRow, Select, SelectItem, Radio} from 'flood-ui-kit';
import {Checkbox, Form, FormRow} from 'flood-ui-kit';
import {FormattedMessage, injectIntl} from 'react-intl';
import React from 'react';
@@ -73,7 +73,7 @@ class DiskUsageTab extends SettingsTab {
this.updateSettings(items);
};
renderDiskItem = (item, index) => {
renderDiskItem = item => {
const {id, visible} = item;
let checkbox = null;

View File

@@ -1,8 +1,8 @@
import {FormattedMessage} from 'react-intl';
import classnames from 'classnames';
import React from 'react';
import stringUtil from 'universally-shared-code/util/stringUtil';
import torrentStatusMap from 'universally-shared-code/constants/torrentStatusMap';
import stringUtil from '@shared/util/stringUtil';
import torrentStatusMap from '@shared/constants/torrentStatusMap';
import ClockIcon from '../../icons/ClockIcon';
import DownloadThickIcon from '../../icons/DownloadThickIcon';

View File

@@ -1,6 +1,6 @@
import classnames from 'classnames';
import {defineMessages, injectIntl} from 'react-intl';
import formatUtil from 'universally-shared-code/util/formatUtil';
import formatUtil from '@shared/util/formatUtil';
import moment from 'moment';
import React from 'react';

View File

@@ -1,4 +1,4 @@
import objectUtil from 'universally-shared-code/util/objectUtil';
import objectUtil from '@shared/util/objectUtil';
const actionTypes = [
'AUTH_CREATE_USER_SUCCESS',

View File

@@ -1,4 +1,4 @@
import objectUtil from 'universally-shared-code/util/objectUtil';
import objectUtil from '@shared/util/objectUtil';
const eventTypes = [
'ALERTS_CHANGE',

View File

@@ -1,4 +1,4 @@
import diffActionTypes from 'universally-shared-code/constants/diffActionTypes';
import diffActionTypes from '@shared/constants/diffActionTypes';
import ActionTypes from '../constants/ActionTypes';
import AppDispatcher from '../dispatcher/AppDispatcher';

View File

@@ -1,4 +1,4 @@
import serverEventTypes from 'universally-shared-code/constants/serverEventTypes';
import serverEventTypes from '@shared/constants/serverEventTypes';
import ActionTypes from '../constants/ActionTypes';
import AlertStore from './AlertStore';

View File

@@ -1,4 +1,4 @@
import diffActionTypes from 'universally-shared-code/constants/diffActionTypes';
import diffActionTypes from '@shared/constants/diffActionTypes';
import ActionTypes from '../constants/ActionTypes';
import AppDispatcher from '../dispatcher/AppDispatcher';

View File

@@ -1,61 +0,0 @@
import React from 'react';
const connectStores = (Component, getEventListenerDescriptors) => {
class ConnectedComponent extends React.Component {
eventHandlersByStore = new Map();
constructor(props) {
super(props);
this.state = getEventListenerDescriptors(props).reduce((state, eventListenerDescriptor) => {
const {store, getValue} = eventListenerDescriptor;
return {
...state,
...getValue({state, props, store, payload: null}),
};
}, {});
}
componentDidMount() {
const eventListenerDescriptors = getEventListenerDescriptors(this.props);
eventListenerDescriptors.forEach(eventListenerDescriptor => {
const {store, event, getValue} = eventListenerDescriptor;
const eventHandler = payload => this.setState((state, props) => getValue({state, props, store, payload}));
const events = Array.isArray(event) ? event : [event];
events.forEach(storeEvent => {
store.listen(storeEvent, eventHandler);
});
if (this.eventHandlersByStore.get(store) == null) {
this.eventHandlersByStore.set(store, new Set());
}
this.eventHandlersByStore.get(store).add({
events,
eventHandler,
});
});
}
componentWillUnmount() {
this.eventHandlersByStore.forEach((listenerDescriptors, store) => {
listenerDescriptors.forEach(({events, eventHandler}) => {
events.forEach(event => {
store.unlisten(event, eventHandler);
});
});
});
this.eventHandlersByStore.clear();
}
render() {
return <Component {...this.props} {...this.state} />;
}
}
return props => <ConnectedComponent {...props} />;
};
export default connectStores;

View File

@@ -0,0 +1,112 @@
import React from 'react';
import EventTypes from '../constants/EventTypes';
interface GenericStore {
listen: (event: keyof typeof EventTypes, eventHandler: (payload: unknown) => void) => void;
unlisten: (event: keyof typeof EventTypes, eventHandler: (payload: unknown) => void) => void;
}
export interface EventListenerDescriptor<DerivedState, WrappedComponentProps = {}> {
store: GenericStore;
event: (keyof typeof EventTypes) | (keyof typeof EventTypes)[];
getValue: (
props: {
payload: unknown;
props: WrappedComponentProps;
state: DerivedState;
store: GenericStore;
},
) => Partial<DerivedState>;
}
const connectStores = <DerivedState extends object, WrappedComponentProps extends object = {}>(
InputComponent: React.JSXElementConstructor<WrappedComponentProps & DerivedState>,
getEventListenerDescriptors: (
props: WrappedComponentProps,
) => EventListenerDescriptor<DerivedState, WrappedComponentProps>[],
): ((props: WrappedComponentProps) => React.ReactElement<WrappedComponentProps>) => {
class ConnectedComponent extends React.Component<WrappedComponentProps, DerivedState> {
private eventHandlersByStore: Map<
GenericStore,
Set<{events: (keyof typeof EventTypes)[]; eventHandler: (payload: unknown) => void}>
> = new Map();
private constructor(props: WrappedComponentProps) {
super(props);
this.state = getEventListenerDescriptors(props).reduce(
(state, eventListenerDescriptor): DerivedState => {
const {store, getValue} = eventListenerDescriptor;
return {
...state,
...getValue({state, props, store, payload: null}),
};
},
({} as unknown) as DerivedState,
);
}
public componentDidMount(): void {
const eventListenerDescriptors = getEventListenerDescriptors(this.props);
eventListenerDescriptors.forEach(
(eventListenerDescriptor): void => {
const {store, event, getValue} = eventListenerDescriptor;
const eventHandler = (payload: unknown): void =>
this.setState(
(state: DerivedState, props: WrappedComponentProps): DerivedState =>
getValue({state, props, store, payload}) as DerivedState,
);
const events = Array.isArray(event) ? event : [event];
events.forEach(
(storeEvent): void => {
store.listen(storeEvent, eventHandler);
},
);
if (this.eventHandlersByStore.get(store) == null) {
const newSet: Set<{
events: (keyof typeof EventTypes)[];
eventHandler: (payload: unknown) => void;
}> = new Set();
this.eventHandlersByStore.set(store, newSet);
}
const eventHandlersForStore = this.eventHandlersByStore.get(store);
if (eventHandlersForStore != null) {
eventHandlersForStore.add({events, eventHandler});
}
},
);
}
public componentWillUnmount(): void {
this.eventHandlersByStore.forEach(
(listenerDescriptors, store): void => {
listenerDescriptors.forEach(
({events, eventHandler}): void => {
events.forEach(
(event): void => {
store.unlisten(event, eventHandler);
},
);
},
);
},
);
this.eventHandlersByStore.clear();
}
public render(): React.ReactNode {
return <InputComponent {...this.props as WrappedComponentProps} {...this.state as DerivedState} />;
}
}
return (props: WrappedComponentProps): React.ReactElement<WrappedComponentProps> => {
return <ConnectedComponent {...props} />;
};
};
export default connectStores;

View File

@@ -1,4 +1,4 @@
import torrentStatusMap from 'universally-shared-code/constants/torrentStatusMap';
import torrentStatusMap from '@shared/constants/torrentStatusMap';
export function filterTorrents(torrentList, opts) {
const {type, filter} = opts;

View File

@@ -0,0 +1,8 @@
import {createBrowserHistory} from 'history';
import stringUtil from '@shared/util/stringUtil';
import ConfigStore from '../stores/ConfigStore';
const history = createBrowserHistory({basename: stringUtil.withoutTrailingSlash(ConfigStore.getBaseURI())});
export default history;

View File

@@ -1,5 +1,5 @@
import classnames from 'classnames';
import torrentStatusMap from 'universally-shared-code/constants/torrentStatusMap';
import torrentStatusMap from '@shared/constants/torrentStatusMap';
export function torrentStatusClasses(torrent, ...classes) {
return classnames(classes, {

View File

@@ -1,5 +1,5 @@
import React from 'react';
import torrentStatusMap from 'universally-shared-code/constants/torrentStatusMap';
import torrentStatusMap from '@shared/constants/torrentStatusMap';
import ErrorIcon from '../components/icons/ErrorIcon';
import SpinnerIcon from '../components/icons/SpinnerIcon';

View File

@@ -1,4 +1,4 @@
import regEx from 'universally-shared-code/util/regEx';
import regEx from '@shared/util/regEx';
export const isNotEmpty = value => {
return value != null && value !== '';

527
client/src/sass/style.scss.d.ts vendored Normal file
View File

@@ -0,0 +1,527 @@
declare const styles: {
readonly button: string;
readonly 'button--primary': string;
readonly inverse: string;
readonly 'button--secondary': string;
readonly 'button--tertiary': string;
readonly 'button--quaternary': string;
readonly 'button--is-disabled': string;
readonly button__content: string;
readonly icon: string;
readonly 'icon--loading': string;
readonly 'button--is-loading': string;
readonly 'context-menu': string;
readonly 'context-menu--enter': string;
readonly 'context-menu__items': string;
readonly 'context-menu__items--is-up': string;
readonly 'context-menu--enter--active': string;
readonly 'context-menu--exit': string;
readonly 'context-menu--exit--active': string;
readonly 'context-menu__items__padding-surrogate': string;
readonly 'context-menu__items--match-trigger-width': string;
readonly 'context-menu__items--no-padding': string;
readonly 'context-menu__items--no-scrolling': string;
readonly container: string;
readonly error: string;
readonly 'error--is-loading': string;
readonly input: string;
readonly form__row: string;
readonly 'form__row--no-margin': string;
readonly 'form__row--group': string;
readonly 'form__row--align--start': string;
readonly 'form__row--align--center': string;
readonly 'form__row--align--end': string;
readonly 'form__row--justify--start': string;
readonly 'form__row--justify--center': string;
readonly 'form__row--justify--end': string;
readonly form__row__item: string;
readonly 'is-first': string;
readonly 'is-last': string;
readonly 'form__row__item--grow': string;
readonly 'form__row__item--shrink': string;
readonly 'form__row__item--one-eighth': string;
readonly 'form__row__item--one-quarter': string;
readonly 'form__row__item--three-eighths': string;
readonly 'form__row__item--one-half': string;
readonly 'form__row__item--five-eighths': string;
readonly 'form__row__item--three-quarters': string;
readonly 'form__row__item--seven-eighths': string;
readonly checkbox: string;
readonly form__element__wrapper: string;
readonly radio: string;
readonly form__element__label: string;
readonly 'form__element--label-offset': string;
readonly 'form__element--match-textbox-height': string;
readonly 'form__element--has-addon--placed-before': string;
readonly 'form__element--has-addon--count-2': string;
readonly 'form__element--has-addon--placed-after': string;
readonly form__element__addon: string;
readonly form__element: string;
readonly 'icon--stroke': string;
readonly 'form__element__addon--placed-before': string;
readonly 'form__element__addon--index-2': string;
readonly 'form__element__addon--placed-after': string;
readonly 'form__element__addon--is-icon': string;
readonly 'form__element__addon--is-interactive': string;
readonly 'icon--small': string;
readonly 'icon--large': string;
readonly icon__element: string;
readonly 'icon--loading--ring': string;
readonly 'icon__ring-slice': string;
readonly 'input--hidden': string;
readonly 'toggle-input': string;
readonly 'toggle-input__indicator': string;
readonly 'toggle-input__indicator__icon': string;
readonly 'toggle-input--is-active': string;
readonly 'toggle-input__element': string;
readonly overlay: string;
readonly 'overlay--transparent': string;
readonly 'overlay--no-interaction': string;
readonly panel: string;
readonly 'panel--medium': string;
readonly panel__content: string;
readonly panel__header: string;
readonly panel__footer: string;
readonly 'panel__footer--has-border': string;
readonly 'panel--large': string;
readonly 'panel__header--has-border': string;
readonly h1: string;
readonly h2: string;
readonly h3: string;
readonly h4: string;
readonly h5: string;
readonly h6: string;
readonly 'panel__content--has-border--top': string;
readonly 'panel__content--has-border--bottom': string;
readonly 'panel--light': string;
readonly portal: string;
readonly section: string;
readonly section__heading: string;
readonly padded: string;
readonly select: string;
readonly select__button: string;
readonly select__indicator: string;
readonly select__item: string;
readonly 'select__item--is-selected': string;
readonly 'select--is-open': string;
readonly app: string;
readonly application: string;
readonly application__view: string;
readonly application__content: string;
readonly application__panel: string;
readonly 'application__panel--torrent-list': string;
readonly 'is-open': string;
readonly 'application__panel--torrent-details': string;
readonly unit: string;
readonly 'text-overflow': string;
readonly 'copy--lead': string;
readonly 'action-bar': string;
readonly 'action-bar--is-condensed': string;
readonly 'action-bar__item': string;
readonly 'action-bar__item--sort-torrents': string;
readonly dropdown: string;
readonly dropdown__content: string;
readonly 'action-bar__item--torrent-operations': string;
readonly 'action-bar__group': string;
readonly 'action-bar__group--has-divider': string;
readonly actions: string;
readonly action: string;
readonly action__label: string;
readonly 'application__loading-overlay': string;
readonly 'application__loading-overlay-leave': string;
readonly 'application__loading-overlay-leave-active': string;
readonly 'application__entry-barrier': string;
readonly alerts__list: string;
readonly 'alerts__list-leave': string;
readonly 'alerts__list-leave-active': string;
readonly 'alerts__list-enter': string;
readonly 'alerts__list-enter-active': string;
readonly alert: string;
readonly 'is-success': string;
readonly alert__count: string;
readonly 'is-error': string;
readonly alert__content: string;
readonly 'attached-panel': string;
readonly 'attached-panel__content': string;
readonly 'attached-panel__wrapper': string;
readonly 'attached-panel-enter': string;
readonly 'attached-panel-enter-active': string;
readonly 'attached-panel-leave': string;
readonly 'attached-panel-leave-active': string;
readonly 'textbox--has-attached-panel--is-open': string;
readonly badge: string;
readonly menu: string;
readonly menu__item: string;
readonly 'menu__item__label--primary': string;
readonly 'has-action': string;
readonly menu__item__label: string;
readonly menu__item__label__action: string;
readonly 'menu__item__label--secondary': string;
readonly 'menu__item--separator': string;
readonly 'is-selectable': string;
readonly 'is-selected': string;
readonly 'menu-enter': string;
readonly 'fade-in': string;
readonly 'menu-leave': string;
readonly 'fade-out': string;
readonly 'client-stats': string;
readonly 'client-stats__rates': string;
readonly 'client-stats__rate': string;
readonly 'client-stats__rate--download': string;
readonly 'client-stats__rate__data--limit': string;
readonly 'client-stats__rate--upload': string;
readonly 'client-stats__rate__icon': string;
readonly 'client-stats__rate__data--secondary': string;
readonly 'client-stats__rate__data--timestamp': string;
readonly 'client-stats__rate__data--primary': string;
readonly 'is-visible': string;
readonly 'client-stats__graph': string;
readonly 'loading-indicator': string;
readonly 'graph__gradient--bottom': string;
readonly 'graph__gradient--bottom--upload': string;
readonly 'graph__gradient--bottom--download': string;
readonly 'graph__gradient--top': string;
readonly 'graph__gradient--top--upload': string;
readonly 'graph__gradient--top--download': string;
readonly graph__area: string;
readonly graph__line: string;
readonly 'graph__line--upload': string;
readonly 'graph__line--download': string;
readonly graph__circle: string;
readonly 'graph__circle--upload': string;
readonly 'graph__circle--download': string;
readonly 'connection-status': string;
readonly 'connection-status__icon': string;
readonly 'connection-status__copy': string;
readonly 'dependency-list': string;
readonly 'dependency-list__dependency': string;
readonly 'dependency-list__dependency__icon': string;
readonly 'dependency-list__dependency--satisfied': string;
readonly 'directory-tree': string;
readonly 'directory-tree__wrapper': string;
readonly 'directory-tree__wrapper--toolbar-visible': string;
readonly 'directory-tree__selection-toolbar': string;
readonly 'modal__content--nested-scroll__content': string;
readonly 'directory-tree__selection-toolbar__item': string;
readonly 'directory-tree__selection-toolbar__item-count': string;
readonly 'button--download': string;
readonly dropdown__items: string;
readonly dropdown__trigger: string;
readonly dropdown__button: string;
readonly dropdown__value: string;
readonly 'directory-tree__parent-directory': string;
readonly 'icon--disk': string;
readonly 'directory-tree__checkbox': string;
readonly checkbox__decoy: string;
readonly 'directory-tree__tree': string;
readonly 'directory-tree__node': string;
readonly file__label: string;
readonly 'directory-tree__checkbox__item--icon': string;
readonly 'directory-tree__checkbox__item--checkbox': string;
readonly 'directory-tree__node--selected': string;
readonly 'directory-tree__node--directory': string;
readonly 'directory-tree__node--group': string;
readonly 'directory-tree__node--file-list': string;
readonly file: string;
readonly 'icon--file': string;
readonly file__detail: string;
readonly 'file__detail--secondary': string;
readonly 'file__detail--priority': string;
readonly 'directory-tree__checkbox__item': string;
readonly 'icon--folder': string;
readonly file__checkbox: string;
readonly file__name: string;
readonly 'dropdown--direction-up': string;
readonly dropdown__content__container: string;
readonly dropdown__label: string;
readonly dropdown__header: string;
readonly dropdown__item: string;
readonly dropdown__list: string;
readonly 'dropdown--align-right': string;
readonly 'dropdown--match-button-width': string;
readonly 'dropdown--width-small': string;
readonly 'is-expanded': string;
readonly dropzone: string;
readonly dropzone__icon: string;
readonly 'dropzone--is-dragging': string;
readonly 'icon--files': string;
readonly 'icon--files__file--right': string;
readonly 'icon--files__file--left': string;
readonly dropzone__copy: string;
readonly 'dropzone__browse-button': string;
readonly 'dropzone__selected-files': string;
readonly 'interactive-list': string;
readonly 'dropzone__selected-files__file': string;
readonly 'dropzone--with-overlay': string;
readonly dropzone__overlay: string;
readonly 'duration--segment': string;
readonly 'filesystem__directory-list': string;
readonly 'filesystem__directory-list__item': string;
readonly 'filesystem__directory-list__item--parent': string;
readonly 'filesystem__directory-list__item--directory': string;
readonly 'filesystem__directory-list__item--file': string;
readonly 'floating-action__button': string;
readonly 'floating-action__button--search': string;
readonly 'floating-action__group--on-textbox': string;
readonly 'icon--eta': string;
readonly icon__ring: string;
readonly 'icon--information__fill': string;
readonly 'icon--information__ring': string;
readonly 'icon--limits': string;
readonly 'limits__bars--top': string;
readonly 'limits__bars--middle': string;
readonly 'limits__bars--bottom': string;
readonly 'icon--loading-indicator': string;
readonly 'loading-indicator--dots__dot': string;
readonly 'loading-indicator-dots-pulse': string;
readonly 'loading-indicator--dots__dot--center': string;
readonly 'loading-indicator--dots__dot--right': string;
readonly 'icon--spinner': string;
readonly 'spinner-spin': string;
readonly 'interactive-list--loading': string;
readonly 'interactive-list__item': string;
readonly 'interactive-list__icon--action': string;
readonly 'interactive-list__icon--action--warning': string;
readonly 'interactive-list__item--stacked-content': string;
readonly 'interactive-list__label': string;
readonly 'interactive-list__label__text': string;
readonly 'interactive-list__label__tag': string;
readonly tag: string;
readonly 'interactive-list__loading-indicator': string;
readonly 'interactive-list__loading-indicator-enter': string;
readonly 'interactive-list__loading-indicator-enter-active': string;
readonly 'interactive-list__loading-indicator-leave': string;
readonly 'interactive-list__loading-indicator-leave-active': string;
readonly 'interactive-list__icon': string;
readonly 'icon--close': string;
readonly 'interactive-list__detail--primary': string;
readonly 'interactive-list__detail--tertiary': string;
readonly 'interactive-list__detail-list': string;
readonly 'interactive-list__detail-list__item': string;
readonly 'interactive-list__detail-list__item--overflow': string;
readonly 'is-inverse': string;
readonly 'loading-indicator__bar': string;
readonly 'loading-indicator-swipe': string;
readonly 'loading-indicator__bar--1': string;
readonly 'loading-indicator__bar--2': string;
readonly 'loading-indicator__bar--3': string;
readonly mediainfo: string;
readonly mediainfo__toolbar: string;
readonly tooltip__wrapper: string;
readonly 'mediainfo__copy-button': string;
readonly mediainfo__output: string;
readonly modal: string;
readonly modal__overlay: string;
readonly 'modal--align-center': string;
readonly modal__tabs: string;
readonly modal__tab: string;
readonly 'is-active': string;
readonly modal__header: string;
readonly 'modal--tabs-in-header': string;
readonly 'has-tabs': string;
readonly modal__content: string;
readonly modal__content__wrapper: string;
readonly 'modal__content--nested-scroll': string;
readonly 'modal__content--nested-scroll__header': string;
readonly modal__body: string;
readonly 'modal--tabs-in-body': string;
readonly modal__footer: string;
readonly modal__actions: string;
readonly 'modal__button-group': string;
readonly 'modal__animation-enter': string;
readonly 'modal__animation-enter-active': string;
readonly 'modal__animation-leave': string;
readonly 'modal__animation-leave-active': string;
readonly 'modal--vertical': string;
readonly 'modal--size-large': string;
readonly form__section__heading: string;
readonly 'form__section__sub-heading': string;
readonly 'notifications--empty': string;
readonly 'notifications--is-loading': string;
readonly notifications__list: string;
readonly 'notifications__loading-indicator': string;
readonly notifications__badge: string;
readonly notifications__list__item: string;
readonly notifications__toolbar: string;
readonly 'toolbar__item--button': string;
readonly notification__heading: string;
readonly notification__category: string;
readonly 'notification__message__sub-heading': string;
readonly 'peers-list__flag': string;
readonly 'peers-list__flag__image': string;
readonly 'peers-list__flag__text': string;
readonly 'peers-list__encryption': string;
readonly 'priority-meter': string;
readonly 'priority-meter__wrapper': string;
readonly 'priority-meter--max-2': string;
readonly 'priority-meter--level-0': string;
readonly 'priority-meter--level-1': string;
readonly 'priority-meter--level-2': string;
readonly 'priority-meter--max-3': string;
readonly 'priority-meter--level-3': string;
readonly 'progress-bar': string;
readonly 'progress-bar__icon': string;
readonly 'torrent--is-seeding': string;
readonly 'torrent--is-stopped': string;
readonly 'torrent--has-error': string;
readonly 'torrent--is-checking': string;
readonly 'torrent--is-selected': string;
readonly 'progress-bar__fill': string;
readonly 'progress-bar__fill__wrapper': string;
readonly 'candy-stripe': string;
readonly scrollbars__thumb: string;
readonly 'scrollbars__thumb--surrogate': string;
readonly 'is-inverted': string;
readonly scrollbars: string;
readonly search: string;
readonly textbox: string;
readonly 'is-in-use': string;
readonly application__sidebar: string;
readonly 'sidebar__icon-button': string;
readonly 'sidebar__icon-button--interactive': string;
readonly 'sidebar__action--last': string;
readonly sidebar__actions: string;
readonly sidebar__diskusage: string;
readonly 'diskuage__size-avail': string;
readonly 'diskusage__text-row': string;
readonly 'diskusage__details-list': string;
readonly 'diskusage__details-list__item': string;
readonly 'diskusage__details-list__label': string;
readonly 'dropdown--speed-limits': string;
readonly 'sidebar-filter': string;
readonly 'sidebar-filter__item': string;
readonly 'sidebar-filter__item--heading': string;
readonly 'sort-dropdown__item': string;
readonly 'sort-dropdown__indicator': string;
readonly 'sort-dropdown__indicator--asc': string;
readonly 'sortable-list': string;
readonly 'sortable-list__item': string;
readonly 'sortable-list__item--is-dragging': string;
readonly 'sortable-list__item--is-locked': string;
readonly 'sortable-list__item--is-preview': string;
readonly 'icon--error': string;
readonly 'icon--lock': string;
readonly 'sortable-list__content': string;
readonly 'sortable-list__content__wrapper': string;
readonly 'sortable-list__content--primary': string;
readonly 'sortable-list__content--secondary': string;
readonly 'sortable-list__content--secondary__copy': string;
readonly 'table__row--heading': string;
readonly table__heading: string;
readonly 'table__heading--is-sorted': string;
readonly 'table__heading--direction--asc': string;
readonly 'table__heading--fill': string;
readonly table__heading__handle: string;
readonly table__heading__label: string;
readonly 'table__heading__resize-line': string;
readonly 'table__heading__column-fill': string;
readonly table__cell: string;
readonly 'textbox-repeater': string;
readonly toolbar: string;
readonly 'toolbar--dark': string;
readonly 'toolbar--bottom': string;
readonly 'toolbar--top': string;
readonly toolbar__item: string;
readonly 'is-disabled': string;
readonly 'toolbar__item--centered': string;
readonly 'toolbar__item--label': string;
readonly tooltip: string;
readonly tooltip__content: string;
readonly 'tooltip__content--no-padding': string;
readonly 'tooltip__content--padding-surrogate': string;
readonly 'is-interactive': string;
readonly 'tooltip--no-wrap': string;
readonly 'tooltip--position--bottom': string;
readonly 'tooltip--anchor--center': string;
readonly 'tooltip--position--top': string;
readonly 'tooltip--anchor--start': string;
readonly 'tooltip--align--center': string;
readonly 'tooltip--anchor--end': string;
readonly 'tooltip--position--left': string;
readonly 'tooltip--position--right': string;
readonly 'tooltip--is-error': string;
readonly 'torrent-details__heading': string;
readonly 'torrent-details__sub-heading': string;
readonly 'torrent-details__sub-heading__secondary': string;
readonly 'torrent-details__sub-heading__tertiary': string;
readonly 'torrent-details__header': string;
readonly 'is-completed': string;
readonly 'is-stopped': string;
readonly 'torrent-details__action': string;
readonly 'torrent-details__table': string;
readonly 'torrent-details__table__heading--primary': string;
readonly 'torrent-details__table__heading--secondary': string;
readonly 'torrent-details__table__heading--tertiary': string;
readonly 'torrent-details__section': string;
readonly 'torrent-details__section__heading': string;
readonly 'torrent-details__section__null-data': string;
readonly 'torrent-details__section--file-tree': string;
readonly 'directory-tree__node--selectable': string;
readonly 'file__detail--size': string;
readonly 'torrent-details__detail--hash': string;
readonly 'torrent-details__detail__value': string;
readonly 'torrent-details__detail--tags': string;
readonly 'torrent-details__detail': string;
readonly 'torrent-details__table__heading': string;
readonly 'torrent-details__detail__label': string;
readonly 'not-available': string;
readonly torrents: string;
readonly torrents__alert: string;
readonly torrents__alert__wrapper: string;
readonly torrents__alert__action: string;
readonly torrent__list: string;
readonly 'torrent__list__scrollbars--horizontal': string;
readonly 'torrent__list__scrollbars--vertical': string;
readonly torrent__list__wrapper: string;
readonly 'torrent__list--loading-enter': string;
readonly 'torrent__list--loading-enter-active': string;
readonly 'torrent__list--loading-leave': string;
readonly 'torrent__list--loading-leave-active': string;
readonly 'torrent__list--empty': string;
readonly 'view--torrent-list': string;
readonly torrent: string;
readonly 'torrent__more-info': string;
readonly torrent__detail: string;
readonly 'torrent__detail--name': string;
readonly 'torrent__detail--tags': string;
readonly torrent__tag: string;
readonly 'torrent__detail__icon--checkmark': string;
readonly 'torrent__details__section--secondary': string;
readonly 'torrent__details__section--tertiary': string;
readonly 'torrent--is-downloading--actively': string;
readonly 'torrent__detail--downRate': string;
readonly 'torrent-details__sub-heading__tertiary--download': string;
readonly 'torrent--is-uploading--actively': string;
readonly 'torrent__detail--upRate': string;
readonly 'torrent-details__sub-heading__tertiary--upload': string;
readonly 'torrent--is-expanded': string;
readonly 'torrent__detail--eta': string;
readonly torrent__details__section: string;
readonly 'torrent__details__section--quaternary': string;
readonly 'torrent__details__section--primary': string;
readonly torrent__details__section__wrapper: string;
readonly 'torrent__detail--percentComplete': string;
readonly 'torrent__detail--upTotal': string;
readonly 'torrent__detail--sizeBytes': string;
readonly 'torrent__detail--freeDiskSpace': string;
readonly 'torrent__detail--added': string;
readonly 'torrent__detail--creationDate': string;
readonly 'torrent__detail--isPrivate': string;
readonly 'torrent__detail--peers': string;
readonly 'torrent__detail--ratio': string;
readonly 'torrent__detail--seeds': string;
readonly torrent__tags: string;
readonly 'torrent--is-condensed': string;
readonly 'transfer-data--download': string;
readonly 'transfer-data--upload': string;
readonly 'application__view--auth-form': string;
readonly 'form--authentication': string;
readonly form__wrapper: string;
readonly form__header: string;
readonly form__label: string;
readonly 'form__row--error': string;
readonly form__actions: string;
readonly 'feed-list__feed-label': string;
readonly rotateAroundMidpoint: string;
};
export = styles;

4
custom.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module '*.svg' {
const content: any;
export default content;
}

2621
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,12 +10,13 @@
"scripts": {
"build": "node client/scripts/build.js",
"build-assets": "UPDATED_SCRIPT=build npm run deprecated-warning && npm run build",
"build-docs": "./node_modules/.bin/jsdoc -c ./.jsdoc.json",
"build-docs": "jsdoc -c ./.jsdoc.json",
"deprecated-warning": "node client/scripts/deprecated-warning.js && sleep 10",
"format-source": "node scripts/prettier.js format",
"format-source": "node scripts/prettier.js formatSource",
"check-source-formatting": "node scripts/prettier.js check",
"lint": "NODE_ENV=development eslint --max-warnings 0 .",
"start": "node --use_strict server/bin/start.js",
"check-types": "tsc",
"lint": "NODE_ENV=development eslint --max-warnings 0 . --ext .js --ext .jsx --ext .ts --ext .tsx",
"start": "node --use_strict server/bin/start.js",
"start:development": "UPDATED_SCRIPT=start:development:server npm run deprecated-warning && npm run start:development:server",
"start:development:client": "node client/scripts/start.js",
"start:development:server": "NODE_ENV=development nodemon server/bin/start.js",
@@ -28,10 +29,12 @@
"@babel/preset-env": "^7.1.0",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.3.3",
"argon2": "^0.19.3",
"@types/react-dom": "^16.8.4",
"@types/react-intl": "^2.3.18",
"@typescript-eslint/parser": "^1.11.0",
"argon2": "^0.24.1",
"autoprefixer": "^8.6.5",
"axios": "^0.18.0",
"babel-cli": "^6.26.0",
"axios": "^0.19.0",
"babel-eslint": "^10.0.3",
"babel-loader": "^8.0.6",
"body-parser": "^1.18.3",
@@ -41,14 +44,13 @@
"clipboard": "^2.0.4",
"compression": "^1.7.3",
"cookie-parser": "^1.4.3",
"css-loader": "^2.1.1",
"css-loader": "^3.0.0",
"d3": "^3.5.17",
"debug": "^3.2.6",
"deep-equal": "^1.0.1",
"express": "^4.16.4",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"feedsub": "^0.6.0",
"file-loader": "1.1.11",
"file-loader": "^4.0.0",
"flood-ui-kit": "^0.1.8",
"flux": "^3.1.3",
"fs-extra": "^5.0.0",
@@ -66,9 +68,9 @@
"node-sass": "^4.11.0",
"object-assign": "4.1.1",
"ospath": "^1.2.2",
"pascal-case": "^2.0.1",
"passport": "^0.4.0",
"passport-jwt": "^4.0.0",
"postcss-flexbugs-fixes": "^3.3.0",
"postcss-loader": "2.1.3",
"promise": "^8.0.2",
"pug": "^2.0.4",
@@ -82,39 +84,50 @@
"react-dropzone": "^4.3.0",
"react-intl": "^2.7.2",
"react-markdown": "^3.6.0",
"react-router": "^3.2.1",
"react-router": "^4.3.1",
"ress": "^1.2.2",
"rimraf": "^2.6.2",
"run-series": "^1.1.6",
"sass-loader": "^6.0.7",
"sass-loader": "^7.1.0",
"saxen": "8.1.x",
"source-map-loader": "^0.2.4",
"spdy": "^3.4.7",
"style-loader": "0.20.3",
"svg-sprite-loader": "^4.1.6",
"tar-stream": "^1.6.2",
"terser-webpack-plugin": "^1.2.3",
"url-loader": "1.0.1",
"typed-css-modules": "^0.5.1",
"url-loader": "^2.0.1",
"xmlrpc": "^1.3.2"
},
"devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.4.4",
"@types/classnames": "^2.2.7",
"@types/keymirror": "^0.1.1",
"@types/node": "^11.11.0",
"@types/react": "^16.8.7",
"@types/react-router-dom": "^4.3.1",
"@types/react-transition-group": "^2.0.16",
"@typescript-eslint/eslint-plugin": "^1.4.2",
"babel-jest": "22.4.3",
"eslint": "^4.19.1",
"eslint": "^5.8.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-config-prettier": "^4.2.0",
"eslint-config-react-app": "^2.1.0",
"eslint-import-resolver-webpack": "^0.11.1",
"eslint-loader": "2.0.0",
"eslint-loader": "^2.1.2",
"eslint-plugin-flowtype": "2.46.1",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-react": "^7.12.4",
"glob": "^7.1.4",
"jest": "22.4.3",
"jsdoc": "~3.5.5",
"jsdoc": "^3.6.2",
"minami": "^1.2.3",
"nodemon": "^1.18.7",
"prettier": "1.14.2",
"react-dev-utils": "^9.0.4",
"typescript": "^3.5.2",
"webpack": "^4.30.0",
"webpack-cli": "^3.3.9",
"webpack-dev-server": "^3.3.1",

View File

@@ -4,82 +4,105 @@ const glob = require('glob');
const path = require('path');
const prettier = require('prettier');
const filePattern = `{client,scripts,server,shared}${path.sep}!(assets){${path.sep},}{**${path.sep}*,*}.{js,json,md}`;
const SOURCE_PATTERN = `{client,scripts,server,shared}${path.sep}!(assets){${path.sep},}{**${
path.sep
}*,*}.{js,jsx,ts,tsx,json,md}`;
const iterateOverFiles = ({onFileRead}) =>
new Promise((resolve, reject) => {
glob(filePattern, (error, files) => {
if (error) {
reject(Error(error));
const readFile = filePath => {
return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf8', (error, fileContent) => {
if (error != null) {
reject(error);
return;
}
resolve(
Promise.all(
files.map(file => {
const filePath = path.join(process.cwd(), file);
const fileContents = fs.readFileSync(filePath, 'utf8');
return onFileRead(filePath, fileContents);
}),
),
);
resolve(fileContent);
});
});
const format = () => {
console.log(chalk.reset('Formatting files...'));
iterateOverFiles({
onFileRead: (filePath, fileContents) =>
prettier.resolveConfig(filePath).then(
options =>
new Promise((resolve, reject) => {
fs.writeFile(filePath, prettier.format(fileContents, {...options, filepath: filePath}), error => {
if (error) {
reject(error);
return;
}
resolve();
});
}),
),
})
.then(() => {
console.log(chalk.green('Done formatting files.'));
})
.catch(error => {
console.log(chalk.red('Error formatting files:\n'), chalk.reset(error));
process.exit(1);
});
};
const check = () => {
console.log(chalk.reset('Checking code formatting...'));
const writeFile = (filePath, fileContent) => {
return new Promise((resolve, reject) => {
fs.writeFile(filePath, fileContent, writeFileError => {
if (writeFileError) {
reject(writeFileError);
return;
}
iterateOverFiles({
onFileRead: (filePath, fileContents) =>
prettier.resolveConfig(filePath).then(options => {
const isCompliant = prettier.check(fileContents, {...options, filepath: filePath});
resolve(filePath);
});
});
};
const formatFile = async (inputFilePath, outputFilePath) => {
const fileContent = await readFile(inputFilePath);
const prettierConfig = await prettier.resolveConfig(inputFilePath);
const writtenFilePath = await writeFile(
outputFilePath,
prettier.format(fileContent, {...prettierConfig, filepath: inputFilePath}),
);
return writtenFilePath;
};
const getSourceFilePaths = () => {
return new Promise((resolve, reject) => {
glob(SOURCE_PATTERN, (error, files) => {
if (error) {
reject(error);
return;
}
resolve(files.map(filePath => path.join(process.cwd(), filePath)));
});
});
};
const formatSource = async () => {
console.log(chalk.reset('Formatting source files...'));
try {
const sourceFilePaths = await getSourceFilePaths();
const formattedPaths = await Promise.all(sourceFilePaths.map(filePath => formatFile(filePath, filePath)));
console.log(chalk.green(`Formatted ${formattedPaths.length} files.`));
} catch (error) {
console.log(chalk.red('Problem formatting file:\n'), chalk.reset(error));
process.exit(1);
}
};
const check = async () => {
console.log(chalk.reset('Validating source file formatting...'));
try {
const sourceFilePaths = await getSourceFilePaths();
await Promise.all(
sourceFilePaths.map(async filePath => {
const fileContent = await readFile(filePath);
const prettierConfig = await prettier.resolveConfig(filePath);
const isCompliant = prettier.check(fileContent, {...prettierConfig, filepath: filePath});
if (!isCompliant) {
throw filePath;
}
}),
})
.then(() => {
console.log(chalk.green('Done checking files.'));
})
.catch(error => {
console.log(chalk.red('Unformatted file found:\n'), chalk.reset(error));
process.exit(1);
});
);
console.log(chalk.green('Finished validating file formatting.'));
} catch (error) {
console.log(chalk.red('Unformatted file found:\n'), chalk.reset(error));
process.exit(1);
}
};
const commands = {check, format};
const commands = {check, formatSource};
const desiredCommand = process.argv.slice(2)[0];
if (commands[desiredCommand] == null) {
throw new Error(`No command ${desiredCommand}.`);
} else {
if (commands[desiredCommand] != null) {
commands[desiredCommand]();
}
module.exports = {
formatFile,
};

View File

@@ -11,4 +11,6 @@ module.exports = {
return string;
},
withoutTrailingSlash: input => input.replace(/\/{1,}$/, ''),
};

19
tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"jsx": "preserve",
"sourceMap": true,
"target": "esnext",
"moduleResolution": "node",
"allowJs": true,
"noEmit": true,
"strict": true,
"isolatedModules": true,
"esModuleInterop": true,
"baseUrl": "./",
"paths": {
"@shared/*": ["shared/*"]
}
},
"include": ["./client/**/*.ts", "./client/**/*.tsx", "./server/**/*.ts", "./server/**/*.tsx", "./custom.d.ts"],
"exclude": ["node_modules", "**/*.spec.ts"]
}