From ecf7b4732bafdddeb9ed405cd8e7d6ef74ac037e Mon Sep 17 00:00:00 2001 From: weizhiqimail Date: Fri, 26 Feb 2021 10:27:26 +0800 Subject: [PATCH] :alien: update webpack config and react page demo. --- .eslintrc.js | 1 + package.json | 2 + scripts/helper.js | 15 +- scripts/paths.js | 68 ++++++++ scripts/plugins/ModuleNotFoundPlugin.js | 146 ++++++++++++++++++ .../plugins/WatchMissingNodeModulesPlugin.js | 33 ++++ scripts/webpack.config.dev.js | 16 +- scripts/webpack.config.js | 18 ++- scripts/webpack.config.prod.js | 8 +- src/components/nav/index.less | 1 + src/components/nav/index.tsx | 17 ++ src/components/nav/types.ts | 4 + src/index.less | 4 + src/index.tsx | 15 +- src/pages/login/index.less | 4 + src/pages/login/index.tsx | 13 ++ src/pages/login/types.ts | 8 + src/store/history.ts | 4 +- yarn.lock | 29 +++- 19 files changed, 379 insertions(+), 27 deletions(-) create mode 100644 scripts/paths.js create mode 100644 scripts/plugins/ModuleNotFoundPlugin.js create mode 100644 scripts/plugins/WatchMissingNodeModulesPlugin.js create mode 100644 src/components/nav/index.less create mode 100644 src/components/nav/index.tsx create mode 100644 src/components/nav/types.ts create mode 100644 src/index.less create mode 100644 src/pages/login/index.less create mode 100644 src/pages/login/index.tsx create mode 100644 src/pages/login/types.ts diff --git a/.eslintrc.js b/.eslintrc.js index 09b35cd..abdd915 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -27,5 +27,6 @@ module.exports = { 'semi': ['error', 'always'], 'no-mixed-spaces-and-tabs': 'off', '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', } }; diff --git a/package.json b/package.json index ca6823f..1eb1eda 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "autoprefixer": "^10.2.4", "babel-loader": "^8.2.2", "babel-plugin-import": "^1.13.3", + "case-sensitive-paths-webpack-plugin": "^2.4.0", "chalk": "^4.1.0", "clean-webpack-plugin": "^3.0.0", "cross-env": "^7.0.3", @@ -59,6 +60,7 @@ "eslint-plugin-promise": "^4.3.1", "eslint-plugin-react": "^7.22.0", "eslint-plugin-standard": "^5.0.0", + "find-up": "^5.0.0", "html-webpack-plugin": "^5.2.0", "less": "^4.1.1", "less-loader": "^8.0.0", diff --git a/scripts/helper.js b/scripts/helper.js index 963b53f..3aa8e1f 100644 --- a/scripts/helper.js +++ b/scripts/helper.js @@ -1,11 +1,11 @@ const fs = require('fs'); -const path = require('path'); const os = require('os'); const dotenv = require('dotenv'); +const paths = require('./paths'); -const NODE_ENV = process.env.NODE_ENV; +const pkg = require(paths.appPackageJson); -const rootPath = path.resolve(__dirname, '..'); +const NODE_ENV = process.env.NODE_ENV; function getProcessEnv() { const REACT_APP_REGEXP = /^REACT_APP_/i; @@ -17,10 +17,10 @@ function getProcessEnv() { const result = {}; const dotenvFiles = [ - `${rootPath}/.env`, - `${rootPath}/.env.local`, - `${rootPath}/.env.${NODE_ENV}.local`, - `${rootPath}/.env.${NODE_ENV}`, + `${paths.appPath}/.env`, + `${paths.appPath}/.env.local`, + `${paths.appPath}/.env.${NODE_ENV}.local`, + `${paths.appPath}/.env.${NODE_ENV}`, ].filter(Boolean); dotenvFiles.forEach(dotenvFile => { @@ -35,6 +35,7 @@ function getProcessEnv() { }); result.NODE_ENV = NODE_ENV; + result.packageName = pkg.name; return result; } diff --git a/scripts/paths.js b/scripts/paths.js new file mode 100644 index 0000000..2f25435 --- /dev/null +++ b/scripts/paths.js @@ -0,0 +1,68 @@ +const path = require('path'); +const fs = require('fs'); + +// 根目录 +const appDirectory = fs.realpathSync(process.cwd()); + +// 依据根目录,找到相对文件或相对目录 +const resolveApp = relativePath => path.resolve(appDirectory, relativePath); + +const moduleFileExtensions = [ + 'web.mjs', + 'mjs', + 'web.js', + 'js', + 'web.ts', + 'ts', + 'web.tsx', + 'tsx', + 'json', + 'web.jsx', + 'jsx', +]; + +const resolveModule = (resolveFn, filePath) => { + const extension = moduleFileExtensions.find(extension => { + return fs.existsSync(resolveFn(`${filePath}.${extension}`)) + } + ); + + if (extension) { + return resolveFn(`${filePath}.${extension}`); + } + + return resolveFn(`${filePath}.js`); +}; + +module.exports = { + // 解析 env 环境变量 + dotenv: resolveApp('.env'), + // 项目根目录 + appPath: resolveApp('.'), + // 项目打包的目录 + appBuild: resolveApp('build'), + // public 资源目录 + appPublic: resolveApp('public'), + // public 目录下的 index.html 文件 + appHtml: resolveApp('public/index.html'), + // 解析入口文件,入口文件可能是 index.js, index.jsx, index.ts, index.tsx + appIndexJs: resolveModule(resolveApp, 'src/index'), + // package.json 的路径 + appPackageJson: resolveApp('package.json'), + // src 目录 + appSrc: resolveApp('src'), + // tsconfig 的路径 + appTsConfig: resolveApp('tsconfig.json'), + // jsconfig 的路径 + appJsConfig: resolveApp('jsconfig.json'), + // yarn.lock 文件的路径 + yarnLockFile: resolveApp('yarn.lock'), + // setupTests 文件的路径 + testsSetup: resolveModule(resolveApp, 'src/setupTests'), + // setupProxy 文件的路径 + proxySetup: resolveApp('src/setupProxy.js'), + // node_modules 的目录路径 + appNodeModules: resolveApp('node_modules'), + // service-worker 文件的路径 + swSrc: resolveModule(resolveApp, 'src/service-worker'), +}; diff --git a/scripts/plugins/ModuleNotFoundPlugin.js b/scripts/plugins/ModuleNotFoundPlugin.js new file mode 100644 index 0000000..da8280a --- /dev/null +++ b/scripts/plugins/ModuleNotFoundPlugin.js @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +const chalk = require('chalk'); +const findUp = require('find-up'); +const path = require('path'); + +class ModuleNotFoundPlugin { + constructor(appPath, yarnLockFile) { + this.appPath = appPath; + this.yarnLockFile = yarnLockFile; + + this.useYarnCommand = this.useYarnCommand.bind(this); + this.getRelativePath = this.getRelativePath.bind(this); + this.prettierError = this.prettierError.bind(this); + } + + useYarnCommand() { + try { + return findUp.sync('yarn.lock', { cwd: this.appPath }) != null; + } catch (_) { + return false; + } + } + + getRelativePath(_file) { + let file = path.relative(this.appPath, _file); + if (file.startsWith('..')) { + file = _file; + } else if (!file.startsWith('.')) { + file = '.' + path.sep + file; + } + return file; + } + + prettierError(err) { + let { details: _details = '', origin } = err; + + if (origin == null) { + const caseSensitivity = + err.message && + /\[CaseSensitivePathsPlugin\] `(.*?)` .* `(.*?)`/.exec(err.message); + if (caseSensitivity) { + const [, incorrectPath, actualName] = caseSensitivity; + const actualFile = this.getRelativePath( + path.join(path.dirname(incorrectPath), actualName) + ); + const incorrectName = path.basename(incorrectPath); + err.message = `Cannot find file: '${incorrectName}' does not match the corresponding name on disk: '${actualFile}'.`; + } + return err; + } + + const file = this.getRelativePath(origin.resource); + let details = _details.split('\n'); + + const request = /resolve '(.*?)' in '(.*?)'/.exec(details); + if (request) { + const isModule = details[1] && details[1].includes('module'); + const isFile = details[1] && details[1].includes('file'); + + let [, target, context] = request; + context = this.getRelativePath(context); + if (isModule) { + const isYarn = this.useYarnCommand(); + details = [ + `Cannot find module: '${target}'. Make sure this package is installed.`, + '', + 'You can install this package by running: ' + + (isYarn + ? chalk.bold(`yarn add ${target}`) + : chalk.bold(`npm install ${target}`)) + + '.', + ]; + } else if (isFile) { + details = [`Cannot find file '${target}' in '${context}'.`]; + } else { + details = [err.message]; + } + } else { + details = [err.message]; + } + err.message = [file, ...details].join('\n').replace('Error: ', ''); + + const isModuleScopePluginError = + err.error && err.error.__module_scope_plugin; + if (isModuleScopePluginError) { + err.message = err.message.replace('Module not found: ', ''); + } + return err; + } + + apply(compiler) { + const { prettierError } = this; + compiler.hooks.make.intercept({ + register(tap) { + if ( + !(tap.name === 'MultiEntryPlugin' || tap.name === 'SingleEntryPlugin') + ) { + return tap; + } + return Object.assign({}, tap, { + fn: (compilation, callback) => { + tap.fn(compilation, (err, ...args) => { + if (err && err.name === 'ModuleNotFoundError') { + err = prettierError(err); + } + callback(err, ...args); + }); + }, + }); + }, + }); + compiler.hooks.normalModuleFactory.tap('ModuleNotFoundPlugin', nmf => { + nmf.hooks.afterResolve.intercept({ + register(tap) { + if (tap.name !== 'CaseSensitivePathsPlugin') { + return tap; + } + return Object.assign({}, tap, { + fn: (compilation, callback) => { + tap.fn(compilation, (err, ...args) => { + if ( + err && + err.message && + err.message.includes('CaseSensitivePathsPlugin') + ) { + err = prettierError(err); + } + callback(err, ...args); + }); + }, + }); + }, + }); + }); + } +} + +module.exports = ModuleNotFoundPlugin; diff --git a/scripts/plugins/WatchMissingNodeModulesPlugin.js b/scripts/plugins/WatchMissingNodeModulesPlugin.js new file mode 100644 index 0000000..97759a7 --- /dev/null +++ b/scripts/plugins/WatchMissingNodeModulesPlugin.js @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// This webpack plugin ensures `npm install ` forces a project rebuild. +// We’re not sure why this isn't webpack's default behavior. +// See https://github.com/facebook/create-react-app/issues/186. + +'use strict'; + +class WatchMissingNodeModulesPlugin { + constructor(nodeModulesPath) { + this.nodeModulesPath = nodeModulesPath; + } + + apply(compiler) { + compiler.hooks.emit.tap('WatchMissingNodeModulesPlugin', compilation => { + var missingDeps = Array.from(compilation.missingDependencies); + var nodeModulesPath = this.nodeModulesPath; + + // If any missing files are expected to appear in node_modules... + if (missingDeps.some(file => file.includes(nodeModulesPath))) { + // ...tell webpack to watch node_modules recursively until they appear. + compilation.contextDependencies.add(nodeModulesPath); + } + }); + } +} + +module.exports = WatchMissingNodeModulesPlugin; diff --git a/scripts/webpack.config.dev.js b/scripts/webpack.config.dev.js index c841ebf..23d87ef 100644 --- a/scripts/webpack.config.dev.js +++ b/scripts/webpack.config.dev.js @@ -7,6 +7,7 @@ const chalk = require('chalk'); const baseConfig = require('./webpack.config'); const pkg = require('../package.json'); const helper = require('./helper'); +const paths = require('./paths'); const defaultConfig = { port: process.env.REACT_APP_PORT || 18000 @@ -18,13 +19,15 @@ module.exports = merge(baseConfig, { devtool: 'source-map', devServer: { - port: defaultConfig.port, - hot: true, + disableHostCheck: true, compress: true, - clientLogLevel: 'silent', - contentBase: path.resolve(__dirname, '../public'), + contentBase: paths.appPublic, + watchContentBase: true, + hot: true, + transportMode: 'ws', + injectClient: false, + port: defaultConfig.port, historyApiFallback: { - index: path.resolve(__dirname, '../public/index.html'), disableDotRule: true, }, after(app, server) { @@ -68,7 +71,8 @@ module.exports = merge(baseConfig, { new HtmlWebpackPlugin({ template: path.resolve(__dirname, '../public/index.html'), title: pkg.name, - }) + }), + ] }); diff --git a/scripts/webpack.config.js b/scripts/webpack.config.js index ae414f6..ca8e5a0 100644 --- a/scripts/webpack.config.js +++ b/scripts/webpack.config.js @@ -1,12 +1,18 @@ const path = require('path'); const webpack = require('webpack'); - +// 大小写敏感 +const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); +// 模块没找到 +const ModuleNotFoundPlugin = require('./plugins/ModuleNotFoundPlugin'); +// 监控模块缺失 +const WatchMissingNodeModulesPlugin = require('./plugins/WatchMissingNodeModulesPlugin'); const helper = require('./helper'); +const paths = require('./paths'); const envObj = helper.getProcessEnv(); module.exports = { - entry: path.resolve(__dirname, '../src/index.tsx'), + entry: paths.appIndexJs, module: { rules: [ @@ -25,8 +31,8 @@ module.exports = { resolve: { alias: { - '@': path.resolve(__dirname, '../src'), - '~': path.resolve(__dirname, '../node_modules'), + '@': paths.appSrc, + '~': paths.appNodeModules, }, extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'] }, @@ -34,6 +40,10 @@ module.exports = { plugins: [ new webpack.DefinePlugin({'process.env': JSON.stringify(envObj)}), new webpack.EnvironmentPlugin(envObj), + new CaseSensitivePathsPlugin(), + new ModuleNotFoundPlugin(paths.appPath), + new webpack.HotModuleReplacementPlugin(), + new WatchMissingNodeModulesPlugin(paths.appNodeModules), ], stats: 'minimal', diff --git a/scripts/webpack.config.prod.js b/scripts/webpack.config.prod.js index d5f0f8d..c4ac147 100644 --- a/scripts/webpack.config.prod.js +++ b/scripts/webpack.config.prod.js @@ -10,6 +10,7 @@ const PackingGenerateFilePlugin = require('./plugins/PackingGenerateFilePlugin') const baseConfig = require('./webpack.config'); const pkg = require('../package.json'); +const paths = require('./paths'); module.exports = merge(baseConfig, { mode: 'development', @@ -17,7 +18,7 @@ module.exports = merge(baseConfig, { devtool: false, output: { - path: path.resolve(__dirname, '../dist'), + path: paths.appBuild, filename: 'js/[contenthash:8].js' }, @@ -62,7 +63,7 @@ module.exports = merge(baseConfig, { plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ - template: path.resolve(__dirname, '../public/index.html'), + template: paths.appHtml, title: pkg.name, minify: { removeComments: true, @@ -82,7 +83,8 @@ module.exports = merge(baseConfig, { chunkFilename: 'css/[name].[contenthash:8].chunk.css', }), new webpack.PrefetchPlugin('react'), - new PackingGenerateFilePlugin() + new PackingGenerateFilePlugin(), + new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), ], optimization: { diff --git a/src/components/nav/index.less b/src/components/nav/index.less new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/components/nav/index.less @@ -0,0 +1 @@ + diff --git a/src/components/nav/index.tsx b/src/components/nav/index.tsx new file mode 100644 index 0000000..1d7ff8e --- /dev/null +++ b/src/components/nav/index.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import {NavLink} from 'react-router-dom'; + +import './index.less'; + +function Nav() { + return ( +
+
    +
  • home
  • +
  • login
  • +
+
+ ); +} + +export default Nav; diff --git a/src/components/nav/types.ts b/src/components/nav/types.ts new file mode 100644 index 0000000..89d2049 --- /dev/null +++ b/src/components/nav/types.ts @@ -0,0 +1,4 @@ +import {PropsWithChildren} from 'react'; +import {RouteComponentProps} from 'react-router-dom'; + +export type HomeProps = PropsWithChildren; diff --git a/src/index.less b/src/index.less new file mode 100644 index 0000000..a999a83 --- /dev/null +++ b/src/index.less @@ -0,0 +1,4 @@ +.container { + width: 1200px; + margin: 0 auto; +} diff --git a/src/index.tsx b/src/index.tsx index 89c14f5..63b2787 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,17 +8,24 @@ import {Spin} from 'antd'; import history from '@/store/history'; import store from '@/store'; import '@/assets/style/global.less'; +import './index.less'; +const Nav = React.lazy(() => import('@/components/nav/index')); const Home = React.lazy(() => import('@/pages/home/index')); +const Login = React.lazy(() => import('@/pages/login/index')); ReactDOM.render( }> - - - - +
+
, document.getElementById('root')); diff --git a/src/pages/login/index.less b/src/pages/login/index.less new file mode 100644 index 0000000..0d11b31 --- /dev/null +++ b/src/pages/login/index.less @@ -0,0 +1,4 @@ +img.avatar { + width: 100px; + height: 100px; +} diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx new file mode 100644 index 0000000..64eaf63 --- /dev/null +++ b/src/pages/login/index.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import './index.less'; + +function Login() { + return ( +
+

Login Page

+
+ ); +} + +export default Login; diff --git a/src/pages/login/types.ts b/src/pages/login/types.ts new file mode 100644 index 0000000..3905e0e --- /dev/null +++ b/src/pages/login/types.ts @@ -0,0 +1,8 @@ +import {PropsWithChildren} from 'react'; +import {RouteComponentProps} from 'react-router-dom'; + +import homeActions from '@/store/actions/home'; + +export type IDispatchProps = typeof homeActions; + +export type HomeProps = PropsWithChildren & IDispatchProps; diff --git a/src/store/history.ts b/src/store/history.ts index b688447..2fc00cf 100644 --- a/src/store/history.ts +++ b/src/store/history.ts @@ -1,5 +1,5 @@ -import {createBrowserHistory} from 'history'; +import {createBrowserHistory as createHistory} from 'history'; -const history = createBrowserHistory(); +const history = createHistory(); export default history; diff --git a/yarn.lock b/yarn.lock index d8d68dc..c6e3850 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1886,6 +1886,11 @@ caniuse-lite@^1.0.30001181: resolved "https://registry.npm.taobao.org/caniuse-lite/download/caniuse-lite-1.0.30001190.tgz?cache=0&sync_timestamp=1613799292999&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcaniuse-lite%2Fdownload%2Fcaniuse-lite-1.0.30001190.tgz#acc6d4a53c68be16cfc314d55c9cab637e558cba" integrity sha1-rMbUpTxovhbPwxTVXJyrY35VjLo= +case-sensitive-paths-webpack-plugin@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4" + integrity sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw== + chalk@*, chalk@^4.0.0, chalk@^4.1.0: version "4.1.0" resolved "https://registry.npm.taobao.org/chalk/download/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" @@ -2969,6 +2974,14 @@ find-up@^4.0.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.npm.taobao.org/flat-cache/download/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -3931,6 +3944,13 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + lodash-es@^4.2.1: version "4.17.21" resolved "https://registry.npm.taobao.org/lodash-es/download/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" @@ -4448,7 +4468,7 @@ p-limit@^2.0.0, p-limit@^2.2.0: dependencies: p-try "^2.0.0" -p-limit@^3.1.0: +p-limit@^3.0.2, p-limit@^3.1.0: version "3.1.0" resolved "https://registry.npm.taobao.org/p-limit/download/p-limit-3.1.0.tgz?cache=0&sync_timestamp=1606288549008&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fp-limit%2Fdownload%2Fp-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" integrity sha1-4drMvnjQ0TiMoYxk/qOOPlfjcGs= @@ -4469,6 +4489,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-map@^2.0.0: version "2.1.0" resolved "https://registry.npm.taobao.org/p-map/download/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"