diff --git a/.gitignore b/.gitignore index d02a5a27d5..928f6bf9ae 100644 --- a/.gitignore +++ b/.gitignore @@ -61,5 +61,6 @@ frontend/webpack-stats.dev.json frontend/node_modules frontend/build frontend/package-lock.json +frontend/.eslintcache /.idea diff --git a/frontend/.babelrc b/frontend/.babelrc index 15aa9d00e2..e6b5823e06 100644 --- a/frontend/.babelrc +++ b/frontend/.babelrc @@ -1,8 +1,4 @@ { - "presets": ["react", "es2015", "stage-0"], - "env": { - "development": { - "presets": ["react-hmre"] - } - } + "presets": ["react-app"], + "plugins": ["dynamic-import-node"] } \ No newline at end of file diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index e952492cdd..9cef5a6181 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -1,25 +1,10 @@ { - "env": { - "browser": true, - "es6": true - }, + "root": true, + "plugins": ["react"], "extends": [ - // "plugin:react/recommended", "react-app", "eslint:recommended" ], - "parser": "babel-eslint", - "parserOptions": { - "ecmaFeatures": { - "experimentalObjectRestSpread": true, - "jsx": true - }, - "sourceType": "module" - }, - "plugins": [ - "react", - "jsx-a11y" - ], "rules": { "indent": [ "warn", @@ -39,8 +24,9 @@ "always" ], - - // overwride error to warning + "no-useless-constructor": "off", + "no-restricted-globals": "off", + "no-unused-expressions": "off", "no-case-declarations": "warn", "no-cond-assign": "warn", "no-redeclare": "warn", @@ -49,14 +35,13 @@ "no-unused-vars": "warn", "no-irregular-whitespace": "warn", "no-console": "warn", + "no-self-assign": ["error", {"props": false}], "no-useless-escape": "warn", - "no-trailing-spaces": "warn", - "react/jsx-indent": ["warn", 2], "react/prop-types": "warn", "react/display-name": "warn", - "jsx-a11y/anchor-has-content": "off", - "jsx-a11y/href-no-hash": "off" + "jsx-a11y/href-no-hash": "off", + "jsx-a11y/anchor-is-valid": "off" } } diff --git a/frontend/config/env.js b/frontend/config/env.js index 30a6c7f1b6..3d1411bd0b 100644 --- a/frontend/config/env.js +++ b/frontend/config/env.js @@ -15,13 +15,13 @@ if (!NODE_ENV) { } // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use -var dotenvFiles = [ +const dotenvFiles = [ `${paths.dotenv}.${NODE_ENV}.local`, - `${paths.dotenv}.${NODE_ENV}`, // Don't include `.env.local` for `test` environment // since normally you expect tests to produce the same // results for everyone NODE_ENV !== 'test' && `${paths.dotenv}.local`, + `${paths.dotenv}.${NODE_ENV}`, paths.dotenv, ].filter(Boolean); @@ -42,12 +42,12 @@ dotenvFiles.forEach(dotenvFile => { // We support resolving modules according to `NODE_PATH`. // This lets you use absolute paths in imports inside large monorepos: -// https://github.com/facebookincubator/create-react-app/issues/253. +// https://github.com/facebook/create-react-app/issues/253. // It works similar to `NODE_PATH` in Node itself: // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. -// Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. -// https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 +// Otherwise, we risk importing Node.js core modules into an app instead of webpack shims. +// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 // We also resolve them to make sure all tools using them work consistently. const appDirectory = fs.realpathSync(process.cwd()); process.env.NODE_PATH = (process.env.NODE_PATH || '') @@ -57,7 +57,7 @@ process.env.NODE_PATH = (process.env.NODE_PATH || '') .join(path.delimiter); // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be -// injected into the application via DefinePlugin in Webpack configuration. +// injected into the application via DefinePlugin in webpack configuration. const REACT_APP = /^REACT_APP_/i; function getClientEnvironment(publicUrl) { @@ -77,9 +77,22 @@ function getClientEnvironment(publicUrl) { // This should only be used as an escape hatch. Normally you would put // images into the `src` and `import` them in code to get their paths. PUBLIC_URL: publicUrl, + // We support configuring the sockjs pathname during development. + // These settings let a developer run multiple simultaneous projects. + // They are used as the connection `hostname`, `pathname` and `port` + // in webpackHotDevClient. They are used as the `sockHost`, `sockPath` + // and `sockPort` options in webpack-dev-server. + WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST, + WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH, + WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT, + // Whether or not react-refresh is enabled. + // react-refresh is not 100% stable at this time, + // which is why it's disabled by default. + // It is defined here so it is available in the webpackHotDevClient. + FAST_REFRESH: process.env.FAST_REFRESH !== 'false', } ); - // Stringify all values so we can feed into Webpack DefinePlugin + // Stringify all values so we can feed into webpack DefinePlugin const stringified = { 'process.env': Object.keys(raw).reduce((env, key) => { env[key] = JSON.stringify(raw[key]); diff --git a/frontend/config/getHttpsConfig.js b/frontend/config/getHttpsConfig.js new file mode 100644 index 0000000000..013d493c1b --- /dev/null +++ b/frontend/config/getHttpsConfig.js @@ -0,0 +1,66 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); +const chalk = require('react-dev-utils/chalk'); +const paths = require('./paths'); + +// Ensure the certificate and key provided are valid and if not +// throw an easy to debug error +function validateKeyAndCerts({ cert, key, keyFile, crtFile }) { + let encrypted; + try { + // publicEncrypt will throw an error with an invalid cert + encrypted = crypto.publicEncrypt(cert, Buffer.from('test')); + } catch (err) { + throw new Error( + `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}` + ); + } + + try { + // privateDecrypt will throw an error with an invalid key + crypto.privateDecrypt(key, encrypted); + } catch (err) { + throw new Error( + `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${ + err.message + }` + ); + } +} + +// Read file and throw an error if it doesn't exist +function readEnvFile(file, type) { + if (!fs.existsSync(file)) { + throw new Error( + `You specified ${chalk.cyan( + type + )} in your env, but the file "${chalk.yellow(file)}" can't be found.` + ); + } + return fs.readFileSync(file); +} + +// Get the https config +// Return cert files if provided in env, otherwise just true or false +function getHttpsConfig() { + const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env; + const isHttps = HTTPS === 'true'; + + if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) { + const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE); + const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE); + const config = { + cert: readEnvFile(crtFile, 'SSL_CRT_FILE'), + key: readEnvFile(keyFile, 'SSL_KEY_FILE'), + }; + + validateKeyAndCerts({ ...config, keyFile, crtFile }); + return config; + } + return isHttps; +} + +module.exports = getHttpsConfig; diff --git a/frontend/config/jest/fileTransform.js b/frontend/config/jest/fileTransform.js index 9e4047d358..aab67618c3 100644 --- a/frontend/config/jest/fileTransform.js +++ b/frontend/config/jest/fileTransform.js @@ -1,12 +1,40 @@ 'use strict'; const path = require('path'); +const camelcase = require('camelcase'); // This is a custom Jest transformer turning file imports into filenames. // http://facebook.github.io/jest/docs/en/webpack.html module.exports = { process(src, filename) { - return `module.exports = ${JSON.stringify(path.basename(filename))};`; + const assetFilename = JSON.stringify(path.basename(filename)); + + if (filename.match(/\.svg$/)) { + // Based on how SVGR generates a component name: + // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 + const pascalCaseFilename = camelcase(path.parse(filename).name, { + pascalCase: true, + }); + const componentName = `Svg${pascalCaseFilename}`; + return `const React = require('react'); + module.exports = { + __esModule: true, + default: ${assetFilename}, + ReactComponent: React.forwardRef(function ${componentName}(props, ref) { + return { + $$typeof: Symbol.for('react.element'), + type: 'svg', + ref: ref, + key: null, + props: Object.assign({}, props, { + children: ${assetFilename} + }) + }; + }), + };`; + } + + return `module.exports = ${assetFilename};`; }, }; diff --git a/frontend/config/modules.js b/frontend/config/modules.js new file mode 100644 index 0000000000..d63e41d78d --- /dev/null +++ b/frontend/config/modules.js @@ -0,0 +1,134 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const paths = require('./paths'); +const chalk = require('react-dev-utils/chalk'); +const resolve = require('resolve'); + +/** + * Get additional module paths based on the baseUrl of a compilerOptions object. + * + * @param {Object} options + */ +function getAdditionalModulePaths(options = {}) { + const baseUrl = options.baseUrl; + + if (!baseUrl) { + return ''; + } + + const baseUrlResolved = path.resolve(paths.appPath, baseUrl); + + // We don't need to do anything if `baseUrl` is set to `node_modules`. This is + // the default behavior. + if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { + return null; + } + + // Allow the user set the `baseUrl` to `appSrc`. + if (path.relative(paths.appSrc, baseUrlResolved) === '') { + return [paths.appSrc]; + } + + // If the path is equal to the root directory we ignore it here. + // We don't want to allow importing from the root directly as source files are + // not transpiled outside of `src`. We do allow importing them with the + // absolute path (e.g. `src/Components/Button.js`) but we set that up with + // an alias. + if (path.relative(paths.appPath, baseUrlResolved) === '') { + return null; + } + + // Otherwise, throw an error. + throw new Error( + chalk.red.bold( + "Your project's `baseUrl` can only be set to `src` or `node_modules`." + + ' Create React App does not support other values at this time.' + ) + ); +} + +/** + * Get webpack aliases based on the baseUrl of a compilerOptions object. + * + * @param {*} options + */ +function getWebpackAliases(options = {}) { + const baseUrl = options.baseUrl; + + if (!baseUrl) { + return {}; + } + + const baseUrlResolved = path.resolve(paths.appPath, baseUrl); + + if (path.relative(paths.appPath, baseUrlResolved) === '') { + return { + src: paths.appSrc, + }; + } +} + +/** + * Get jest aliases based on the baseUrl of a compilerOptions object. + * + * @param {*} options + */ +function getJestAliases(options = {}) { + const baseUrl = options.baseUrl; + + if (!baseUrl) { + return {}; + } + + const baseUrlResolved = path.resolve(paths.appPath, baseUrl); + + if (path.relative(paths.appPath, baseUrlResolved) === '') { + return { + '^src/(.*)$': '/src/$1', + }; + } +} + +function getModules() { + // Check if TypeScript is setup + const hasTsConfig = fs.existsSync(paths.appTsConfig); + const hasJsConfig = fs.existsSync(paths.appJsConfig); + + if (hasTsConfig && hasJsConfig) { + throw new Error( + 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' + ); + } + + let config; + + // If there's a tsconfig.json we assume it's a + // TypeScript project and set up the config + // based on tsconfig.json + if (hasTsConfig) { + const ts = require(resolve.sync('typescript', { + basedir: paths.appNodeModules, + })); + config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config; + // Otherwise we'll check if there is jsconfig.json + // for non TS projects. + } else if (hasJsConfig) { + config = require(paths.appJsConfig); + } + + config = config || {}; + const options = config.compilerOptions || {}; + + const additionalModulePaths = getAdditionalModulePaths(options); + + return { + additionalModulePaths: additionalModulePaths, + webpackAliases: getWebpackAliases(options), + jestAliases: getJestAliases(options), + hasTsConfig, + }; +} + +module.exports = getModules(); diff --git a/frontend/config/paths.js b/frontend/config/paths.js index a038007cca..f86494a8a8 100644 --- a/frontend/config/paths.js +++ b/frontend/config/paths.js @@ -2,54 +2,72 @@ const path = require('path'); const fs = require('fs'); -const url = require('url'); +const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath'); // Make sure any symlinks in the project folder are resolved: -// https://github.com/facebookincubator/create-react-app/issues/637 +// https://github.com/facebook/create-react-app/issues/637 const appDirectory = fs.realpathSync(process.cwd()); const resolveApp = relativePath => path.resolve(appDirectory, relativePath); -const envPublicUrl = process.env.PUBLIC_URL; - -function ensureSlash(path, needsSlash) { - const hasSlash = path.endsWith('/'); - if (hasSlash && !needsSlash) { - return path.substr(path, path.length - 1); - } else if (!hasSlash && needsSlash) { - return `${path}/`; - } else { - return path; - } -} - -const getPublicUrl = appPackageJson => - envPublicUrl || require(appPackageJson).homepage; - // We use `PUBLIC_URL` environment variable or "homepage" field to infer // "public path" at which the app is served. -// Webpack needs to know it to put the right {% render_bundle 'commons' %} +{% render_bundle 'runtime' %} {% block extra_script %}{% endblock %} diff --git a/seahub/templates/markdown_file_view_react.html b/seahub/templates/markdown_file_view_react.html index 74f8f86e87..b2e1696fac 100644 --- a/seahub/templates/markdown_file_view_react.html +++ b/seahub/templates/markdown_file_view_react.html @@ -13,6 +13,7 @@ {% render_bundle 'markdownEditor' 'css' %} + {% render_bundle 'commons' 'css' %} {% if branding_css != '' %}{% endif %} {% if enable_branding_css %}{% endif %} @@ -64,7 +65,8 @@ - {% render_bundle 'commons' %} + {% render_bundle 'commons' 'js' %} + {% render_bundle 'runtime' 'js' %} {% render_bundle 'markdownEditor' 'js' %}