├── .github
└── workflows
│ └── codeql-analysis.yml
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── babel.config.json
├── dist
├── index.cjs.js
├── index.esm.js
├── index.umd.js
├── react-formutil.cjs.development.js
├── react-formutil.cjs.production.js
├── react-formutil.esm.development.js
├── react-formutil.esm.production.js
├── react-formutil.umd.development.js
└── react-formutil.umd.production.js
├── docs
├── .gitignore
├── .tern-project
├── .tern-webpack-config.js
├── README.md
├── app
│ ├── index.js
│ └── modules
│ │ ├── LoginForm
│ │ ├── FieldCity.js
│ │ └── index.js
│ │ └── SourceCode
│ │ ├── index.js
│ │ └── style.scss
├── demo
│ ├── index.html
│ └── static
│ │ ├── css
│ │ ├── 5.6b7ad16f.css
│ │ ├── 6.ea2d060c.css
│ │ └── index.88fcec2f.css
│ │ ├── images
│ │ ├── glyphicons-halflings-regular.448c34a5.woff2
│ │ ├── glyphicons-halflings-regular.89889688.svg
│ │ ├── glyphicons-halflings-regular.e18bbf61.ttf
│ │ ├── glyphicons-halflings-regular.f4769f9b.eot
│ │ └── glyphicons-halflings-regular.fa277232.woff
│ │ └── js
│ │ ├── 0.8e7ead2f.js
│ │ ├── 5.34f0eeb4.js
│ │ ├── 6.5034ec00.js
│ │ ├── 7.4fb65dc8.js
│ │ ├── 8.07f14e87.js
│ │ ├── _vendor_.dbf0ba9b.js
│ │ ├── index.d1448da7.js
│ │ ├── runtime.67bf504e.js
│ │ └── vendor.fce07163.js
├── global.d.ts
├── package.json
├── public
│ └── index.html
├── scripts
│ ├── build.js
│ ├── cdn.js
│ ├── config
│ │ ├── checkMissDependencies.js
│ │ ├── env.js
│ │ ├── eslintrc.js
│ │ ├── helper.js
│ │ ├── paths.js
│ │ ├── polyfills.js
│ │ ├── tslintrc.json
│ │ ├── webpack.config.dev.js
│ │ └── webpack.config.prod.js
│ ├── i18n.js
│ └── start.js
├── source
│ ├── LoginForm1
│ │ └── index.js
│ └── LoginForm2
│ │ └── index.js
└── tsconfig.json
├── eslint.config.js
├── hooks.d.ts
├── hooks.js
├── index.d.ts
├── jest
├── cssTransform.js
├── fileTransform.js
├── jest.config.js
├── setupTests.ts
└── test.js
├── npm
├── index.cjs.js
├── index.esm.js
└── index.umd.js
├── package.json
├── rollup.config.js
├── setupTests.ts
├── src
├── EasyField
│ ├── Group.js
│ ├── List.js
│ ├── Native.js
│ ├── easyFieldHandler.js
│ └── index.js
├── Field.js
├── Form.js
├── connect.js
├── context.js
├── fieldHelper.js
├── hooks
│ ├── Field.js
│ ├── useField.js
│ ├── useForm.js
│ ├── useFormContext.js
│ └── useHandler.js
├── index.js
├── utils.js
├── withField.js
└── withForm.js
├── tests
├── EasyField.test.tsx
├── Field.test.tsx
├── Form.test.tsx
├── connect.test.tsx
├── dev.dist.test.tsx
├── helper.tsx
├── prod.dist.test.tsx
├── useField.test.tsx
├── utils.test.ts
├── withField.test.tsx
└── withForm.test.tsx
└── tsconfig.json
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ master ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ master ]
20 | schedule:
21 | - cron: '19 1 * * 3'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 |
28 | strategy:
29 | fail-fast: false
30 | matrix:
31 | language: [ 'javascript', 'typescript' ]
32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
33 | # Learn more:
34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
35 |
36 | steps:
37 | - name: Checkout repository
38 | uses: actions/checkout@v2
39 |
40 | # Initializes the CodeQL tools for scanning.
41 | - name: Initialize CodeQL
42 | uses: github/codeql-action/init@v1
43 | with:
44 | languages: ${{ matrix.language }}
45 | # If you wish to specify custom queries, you can do so here or in a config file.
46 | # By default, queries listed here will override any specified in a config file.
47 | # Prefix the list here with "+" to use these queries and those in the config file.
48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
49 |
50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
51 | # If this step fails, then you should remove it and run the build manually (see below)
52 | - name: Autobuild
53 | uses: github/codeql-action/autobuild@v1
54 |
55 | # ℹ️ Command-line programs to run using the OS shell.
56 | # 📚 https://git.io/JvXDl
57 |
58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
59 | # and modify them (or add more) to build your code if your project
60 | # uses a compiled language
61 |
62 | #- run: |
63 | # make bootstrap
64 | # make release
65 |
66 | - name: Perform CodeQL Analysis
67 | uses: github/codeql-action/analyze@v1
68 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.un~
2 | .DS_Store
3 | node_modules
4 | /package-lock.json
5 | /coverage
6 | /.git-tsconfig.json
7 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | docs/
2 | src/
3 | npm/
4 | jest/
5 | coverage/
6 | tests/
7 | *.un~
8 | /jsconfig.json
9 | /tsconfig.json
10 | /rollup.*
11 | /babel.config.json
12 | /eslint.config.js
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 qiqiboy
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react-app"],
3 | "plugins": [
4 | "react-hot-loader/babel",
5 | [
6 | "@babel/plugin-proposal-decorators",
7 | {
8 | "legacy": true
9 | }
10 | ]
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/dist/index.cjs.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 | module.exports = require('./react-formutil.cjs.production.js');
3 | } else {
4 | module.exports = require('./react-formutil.cjs.development.js');
5 | }
6 |
--------------------------------------------------------------------------------
/dist/index.esm.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 | module.exports = require('./react-formutil.esm.production.js');
3 | } else {
4 | module.exports = require('./react-formutil.esm.development.js');
5 | }
6 |
--------------------------------------------------------------------------------
/dist/index.umd.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 | module.exports = require('./react-formutil.umd.production.js');
3 | } else {
4 | module.exports = require('./react-formutil.umd.development.js');
5 | }
6 |
7 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | *.un~
2 | .*.swp
3 | .*.swo
4 | *.rdb
5 | .DS_Store
6 | node_modules
7 | bower_components
8 | /npm-debug.log
9 | /dump.rdb
10 | Thumbs.db
11 |
12 | # git pre-commit tsc lint
13 | .git-tsconfig.json
--------------------------------------------------------------------------------
/docs/.tern-project:
--------------------------------------------------------------------------------
1 | {
2 | "libs": [
3 | "browser",
4 | "jquery",
5 | "ecma5",
6 | "ecma6",
7 | "underscore",
8 | "angular"
9 | ],
10 | "plugins": {
11 | "angular": {},
12 | "node": {},
13 | "es_modules": {},
14 | "commonjs": {},
15 | "webpack": {
16 | "configPath": "./.tern-webpack-config.js"
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/docs/.tern-webpack-config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const DirectoryNamedWebpackPlugin = require('directory-named-webpack-plugin');
3 |
4 | module.exports = {
5 | resolve: {
6 | modules: [path.resolve(__dirname, 'node_modules'), path.resolve(__dirname)],
7 | alias: {
8 | utils: path.resolve(__dirname, 'app/utils'),
9 | components: path.resolve(__dirname, 'app/components'),
10 | modules: path.resolve(__dirname, 'app/modules')
11 | },
12 | plugins: [
13 | new DirectoryNamedWebpackPlugin({
14 | honorIndex: true,
15 | exclude: /node_modules|libs/
16 | })
17 | ]
18 | }
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | 支持React + ES6的标准开发环境
2 | ================
3 |
4 | ### 开始开发
5 | $ npm start
6 |
7 | 该命令会在本地通过`webpack-dev-server`创建一个本地开发服务,支持hot reload.
8 |
9 | ### 部署测试
10 | $ npm run build:dev
11 |
12 | ### 上线
13 | $ npm run pack
14 |
15 |
--------------------------------------------------------------------------------
/docs/app/index.js:
--------------------------------------------------------------------------------
1 | /* eslint import/no-webpack-loader-syntax: 0*/
2 | import React from 'react';
3 | import { render } from 'react-dom';
4 | import SourceCode from 'modules/SourceCode';
5 | import { HashRouter, Switch, Route, Redirect, NavLink as Link } from 'react-router-dom';
6 | import 'bootstrap-sass/assets/stylesheets/_bootstrap.scss';
7 |
8 | import LoginForm from 'modules/LoginForm';
9 |
10 | const source1 = require('!!raw-loader!source/LoginForm1').default;
11 | const source2 = require('!!raw-loader!source/LoginForm2').default;
12 | const $error = {};
13 |
14 | for (let name in $error) {
15 | if ($error.hasOwenProperty(name)) {
16 | console.log(name);
17 | }
18 | }
19 |
20 | render(
21 |
22 |
23 |
48 |
49 |
50 |
51 | 有两种调用方式,你可以点击切换查看不同的实现代码
52 |
53 |
54 | 源码一
55 |
56 |
57 | 源码二
58 |
59 |
60 |
61 |
62 | } />
63 | } />
64 |
65 |
66 |
67 |
68 | ,
69 | document.getElementById('wrap')
70 | );
71 |
--------------------------------------------------------------------------------
/docs/app/modules/LoginForm/FieldCity.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { withForm, withField, Field } from 'app/../../src';
3 |
4 | const provinceData = [
5 | {
6 | id: 'beijing',
7 | name: '北京',
8 | children: [
9 | {
10 | id: 'chaoyang',
11 | name: '朝阳区'
12 | },
13 | {
14 | id: 'haidian',
15 | name: '海淀区'
16 | }
17 | ]
18 | },
19 | {
20 | id: 'shanghai',
21 | name: '上海',
22 | children: [
23 | {
24 | id: 'pudong',
25 | name: '浦东新区'
26 | },
27 | {
28 | id: 'jingan',
29 | name: '静安区'
30 | }
31 | ]
32 | },
33 | {
34 | id: 'guangdong',
35 | name: '广东',
36 | children: [
37 | {
38 | id: 'guangzhou',
39 | name: '广州市'
40 | },
41 | {
42 | id: 'shenzhen',
43 | name: '深圳市'
44 | }
45 | ]
46 | }
47 | ];
48 |
49 | class FieldCity extends Component {
50 | state = {
51 | loading: true
52 | };
53 |
54 | // 模拟异步获取省份信息
55 | getProvinceData = () => {
56 | this.setState({
57 | loading: true
58 | });
59 |
60 | setTimeout(() => {
61 | this.setState({
62 | provinceData,
63 | loading: false
64 | });
65 |
66 | // 获取到省份信息后,如果有已经选择了省份,需要再去获取下城市列表
67 | if (this.props.$formutil.$params.province) {
68 | this.getCityData(this.props.$formutil.$params.province);
69 | }
70 | }, 1500);
71 | };
72 |
73 | // 模拟异步获取城市信息
74 | getCityData = id => {
75 | if (id) {
76 | this.setState({
77 | loading: true
78 | });
79 |
80 | setTimeout(
81 | () =>
82 | this.setState({
83 | cityData: provinceData.find(item => item.id === id).children,
84 | loading: false
85 | }),
86 | 1500
87 | );
88 | }
89 | };
90 |
91 | onProvinceChange = () => {
92 | this.setState({
93 | cityData: null
94 | });
95 |
96 | this.props.$formutil.$setValues(
97 | {
98 | city: ''
99 | },
100 | () => {
101 | this.onCityChange();
102 | }
103 | );
104 |
105 | if (this.props.$formutil.$params.province) {
106 | this.getCityData(this.props.$formutil.$params.province);
107 | }
108 | };
109 |
110 | onCityChange = () => {
111 | if (this.props.$formutil.$invalid) {
112 | this.props.$fieldutil.$render(null);
113 | } else {
114 | this.props.$fieldutil.$render(this.props.$formutil.$params);
115 | }
116 | };
117 |
118 | fieldProps = {
119 | required: true,
120 | $validators: {
121 | required: Boolean
122 | }
123 | };
124 |
125 | componentDidMount() {
126 | this.getProvinceData();
127 |
128 | if (this.props.$fieldutil.$value) {
129 | this.props.$formutil.$setValues(this.props.$fieldutil.$value);
130 | }
131 | }
132 |
133 | render() {
134 | const { provinceData, cityData, loading } = this.state;
135 | const { province = '', city = '' } = this.props.$fieldutil.$value || this.props.$formutil.$params;
136 |
137 | return (
138 |
139 |
140 |
141 | {props => (
142 |
154 | )}
155 |
156 |
157 |
158 |
159 | {props => (
160 |
172 | )}
173 |
174 |
175 |
176 | );
177 | }
178 | }
179 |
180 | // 这里先包装了withForm,方便可以获取到用户填写信息,再包装withField,是因为我们对外暴漏为一个Field
181 | export default withField(withForm(FieldCity), {
182 | $defaultValue: null
183 | });
184 |
--------------------------------------------------------------------------------
/docs/app/modules/SourceCode/index.js:
--------------------------------------------------------------------------------
1 | /* eslint import/no-webpack-loader-syntax: 0*/
2 | import React, { Component } from 'react';
3 | import './style.scss';
4 |
5 | class SrouceCode extends Component {
6 | async componentDidMount() {
7 | const [{ default: Editor }] = await Promise.all([
8 | import('codemirror'), // 异步载入编辑器代码
9 | import('codemirror/mode/javascript/javascript'),
10 | import('codemirror/mode/css/css'),
11 | import('codemirror/lib/codemirror.css'), // 载入编辑器样式
12 | import('codemirror/theme/solarized.css') // 载入编辑器主题
13 | ]);
14 |
15 | this.jsEditor = new Editor(this.editorNode, {
16 | mode: 'javascript',
17 | lineNumbers: true,
18 | theme: 'solarized',
19 | value: this.props.source,
20 | readOnly: true
21 | });
22 | }
23 |
24 | refCallback = node => {
25 | this.editorNode = node;
26 | };
27 |
28 | render() {
29 | return (
30 |
34 | );
35 | }
36 | }
37 |
38 | export default SrouceCode;
39 |
--------------------------------------------------------------------------------
/docs/app/modules/SourceCode/style.scss:
--------------------------------------------------------------------------------
1 | .CodeMirror {
2 | border: 1px solid #eee;
3 | height: auto !important;
4 | }
5 |
--------------------------------------------------------------------------------
/docs/demo/index.html:
--------------------------------------------------------------------------------
1 | react-formutil
--------------------------------------------------------------------------------
/docs/demo/static/css/5.6b7ad16f.css:
--------------------------------------------------------------------------------
1 | .CodeMirror{font-family:monospace;height:300px;color:#000;direction:ltr}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-fat-cursor-mark{background-color:rgba(20,255,20,.5)}.cm-animate-fat-cursor,.cm-fat-cursor-mark{-webkit-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite}.cm-animate-fat-cursor{width:auto;border:0;background-color:#7e7}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-rulers{position:absolute;left:0;right:0;top:-50px;bottom:-20px;overflow:hidden}.CodeMirror-ruler{border-left:1px solid #ccc;top:0;bottom:0;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-type,.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-invalidchar,.cm-s-default .cm-error{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0b0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#a22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:none;position:relative}.CodeMirror-sizer{position:relative;border-right:30px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-30px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:none!important;border:none!important}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-gutter-wrapper ::selection{background-color:transparent}.CodeMirror-gutter-wrapper ::-moz-selection{background-color:transparent}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre{border-radius:0;border-width:0;background:transparent;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:contextual;-webkit-font-feature-settings:"calt";font-feature-settings:"calt";font-variant-ligatures:contextual}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;padding:.1px}.CodeMirror-rtl pre{direction:rtl}.CodeMirror-code{outline:none}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute;pointer-events:none}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}.CodeMirror-focused div.CodeMirror-cursors,div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background-color:#ffa;background-color:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:""}span.CodeMirror-selectedtext{background:none}
--------------------------------------------------------------------------------
/docs/demo/static/css/6.ea2d060c.css:
--------------------------------------------------------------------------------
1 | .solarized.base03{color:#002b36}.solarized.base02{color:#073642}.solarized.base01{color:#586e75}.solarized.base00{color:#657b83}.solarized.base0{color:#839496}.solarized.base1{color:#93a1a1}.solarized.base2{color:#eee8d5}.solarized.base3{color:#fdf6e3}.solarized.solar-yellow{color:#b58900}.solarized.solar-orange{color:#cb4b16}.solarized.solar-red{color:#dc322f}.solarized.solar-magenta{color:#d33682}.solarized.solar-violet{color:#6c71c4}.solarized.solar-blue{color:#268bd2}.solarized.solar-cyan{color:#2aa198}.solarized.solar-green{color:#859900}.cm-s-solarized{line-height:1.45em;color-profile:sRGB;rendering-intent:auto}.cm-s-solarized.cm-s-dark{color:#839496;background-color:#002b36;text-shadow:#002b36 0 1px}.cm-s-solarized.cm-s-light{background-color:#fdf6e3;color:#657b83;text-shadow:#eee8d5 0 1px}.cm-s-solarized .CodeMirror-widget{text-shadow:none}.cm-s-solarized .cm-header{color:#586e75}.cm-s-solarized .cm-quote{color:#93a1a1}.cm-s-solarized .cm-keyword{color:#cb4b16}.cm-s-solarized .cm-atom,.cm-s-solarized .cm-number{color:#d33682}.cm-s-solarized .cm-def{color:#2aa198}.cm-s-solarized .cm-variable{color:#839496}.cm-s-solarized .cm-variable-2{color:#b58900}.cm-s-solarized .cm-type,.cm-s-solarized .cm-variable-3{color:#6c71c4}.cm-s-solarized .cm-property{color:#2aa198}.cm-s-solarized .cm-operator{color:#6c71c4}.cm-s-solarized .cm-comment{color:#586e75;font-style:italic}.cm-s-solarized .cm-string{color:#859900}.cm-s-solarized .cm-string-2{color:#b58900}.cm-s-solarized .cm-meta{color:#859900}.cm-s-solarized .cm-qualifier{color:#b58900}.cm-s-solarized .cm-builtin{color:#d33682}.cm-s-solarized .cm-bracket{color:#cb4b16}.cm-s-solarized .CodeMirror-matchingbracket{color:#859900}.cm-s-solarized .CodeMirror-nonmatchingbracket{color:#dc322f}.cm-s-solarized .cm-tag{color:#93a1a1}.cm-s-solarized .cm-attribute{color:#2aa198}.cm-s-solarized .cm-hr{color:transparent;border-top:1px solid #586e75;display:block}.cm-s-solarized .cm-link{color:#93a1a1;cursor:pointer}.cm-s-solarized .cm-special{color:#6c71c4}.cm-s-solarized .cm-em{color:#999;text-decoration:underline;-webkit-text-decoration-style:dotted;text-decoration-style:dotted}.cm-s-solarized .cm-error,.cm-s-solarized .cm-invalidchar{color:#586e75;border-bottom:1px dotted #dc322f}.cm-s-solarized.cm-s-dark div.CodeMirror-selected{background:#073642}.cm-s-solarized.cm-s-dark.CodeMirror ::selection{background:rgba(7,54,66,.99)}.cm-s-dark .CodeMirror-line>span::-moz-selection,.cm-s-dark .CodeMirror-line>span>span::-moz-selection,.cm-s-solarized.cm-s-dark .CodeMirror-line::-moz-selection{background:rgba(7,54,66,.99)}.cm-s-solarized.cm-s-light div.CodeMirror-selected{background:#eee8d5}.cm-s-light .CodeMirror-line>span::selection,.cm-s-light .CodeMirror-line>span>span::selection,.cm-s-solarized.cm-s-light .CodeMirror-line::selection{background:#eee8d5}.cm-s-ligh .CodeMirror-line>span::-moz-selection,.cm-s-ligh .CodeMirror-line>span>span::-moz-selection,.cm-s-solarized.cm-s-light .CodeMirror-line::-moz-selection{background:#eee8d5}.cm-s-solarized.CodeMirror{box-shadow:inset 7px 0 12px -6px #000}.cm-s-solarized .CodeMirror-gutters{border-right:0}.cm-s-solarized.cm-s-dark .CodeMirror-gutters{background-color:#073642}.cm-s-solarized.cm-s-dark .CodeMirror-linenumber{color:#586e75;text-shadow:#021014 0 -1px}.cm-s-solarized.cm-s-light .CodeMirror-gutters{background-color:#eee8d5}.cm-s-solarized.cm-s-light .CodeMirror-linenumber{color:#839496}.cm-s-solarized .CodeMirror-linenumber{padding:0 5px}.cm-s-solarized .CodeMirror-guttermarker-subtle{color:#586e75}.cm-s-solarized.cm-s-dark .CodeMirror-guttermarker{color:#ddd}.cm-s-solarized.cm-s-light .CodeMirror-guttermarker{color:#cb4b16}.cm-s-solarized .CodeMirror-gutter .CodeMirror-gutter-text{color:#586e75}.cm-s-solarized .CodeMirror-cursor{border-left:1px solid #819090}.cm-s-solarized.cm-s-light.cm-fat-cursor .CodeMirror-cursor{background:#7e7}.cm-s-solarized.cm-s-light .cm-animate-fat-cursor{background-color:#7e7}.cm-s-solarized.cm-s-dark.cm-fat-cursor .CodeMirror-cursor{background:#586e75}.cm-s-solarized.cm-s-dark .cm-animate-fat-cursor{background-color:#586e75}.cm-s-solarized.cm-s-dark .CodeMirror-activeline-background{background:hsla(0,0%,100%,.06)}.cm-s-solarized.cm-s-light .CodeMirror-activeline-background{background:rgba(0,0,0,.06)}
--------------------------------------------------------------------------------
/docs/demo/static/images/glyphicons-halflings-regular.448c34a5.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qiqiboy/react-formutil/d531c1b6998c3285a87074eefd731943ec3f2a51/docs/demo/static/images/glyphicons-halflings-regular.448c34a5.woff2
--------------------------------------------------------------------------------
/docs/demo/static/images/glyphicons-halflings-regular.e18bbf61.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qiqiboy/react-formutil/d531c1b6998c3285a87074eefd731943ec3f2a51/docs/demo/static/images/glyphicons-halflings-regular.e18bbf61.ttf
--------------------------------------------------------------------------------
/docs/demo/static/images/glyphicons-halflings-regular.f4769f9b.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qiqiboy/react-formutil/d531c1b6998c3285a87074eefd731943ec3f2a51/docs/demo/static/images/glyphicons-halflings-regular.f4769f9b.eot
--------------------------------------------------------------------------------
/docs/demo/static/images/glyphicons-halflings-regular.fa277232.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qiqiboy/react-formutil/d531c1b6998c3285a87074eefd731943ec3f2a51/docs/demo/static/images/glyphicons-halflings-regular.fa277232.woff
--------------------------------------------------------------------------------
/docs/demo/static/js/5.34f0eeb4.js:
--------------------------------------------------------------------------------
1 | (this.webpackJsonp=this.webpackJsonp||[]).push([[5],{HoVT:function(p,s,n){}}]);
--------------------------------------------------------------------------------
/docs/demo/static/js/6.5034ec00.js:
--------------------------------------------------------------------------------
1 | (this.webpackJsonp=this.webpackJsonp||[]).push([[6],{lNMr:function(p,s,n){}}]);
--------------------------------------------------------------------------------
/docs/demo/static/js/_vendor_.dbf0ba9b.js:
--------------------------------------------------------------------------------
1 | /*! @author qiqiboy */
2 | (this.webpackJsonp=this.webpackJsonp||[]).push([[3],[],[[0,1,2]]]);
--------------------------------------------------------------------------------
/docs/demo/static/js/runtime.67bf504e.js:
--------------------------------------------------------------------------------
1 | /*! @author qiqiboy */!function(e){function t(t){for(var n,o,u=t[0],c=t[1],s=t[2],f=0,d=[];f = Pick>;
21 |
22 | /**
23 | * create HOC(Higher Order Component)
24 | *
25 | */
26 | type HOC = (
27 | Component: React.ComponentType
28 | ) => React.ComponentType>;
29 |
30 | /**
31 | * i18n
32 | */
33 | type I18nFunc = (key: string) => string;
34 |
35 | declare const __: I18nFunc;
36 |
37 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-formutil-demo",
3 | "author": "qiqiboy",
4 | "version": "1.0.0",
5 | "private": true,
6 | "vendor": [
7 | "react",
8 | "react-dom"
9 | ],
10 | "noRewrite": true,
11 | "proxy": null,
12 | "scripts": {
13 | "start": "node scripts/start.js",
14 | "build": "node scripts/build.js",
15 | "build:dev": "node scripts/build.js --dev",
16 | "pack": "npm run build",
17 | "count": "node scripts/count.js",
18 | "tsc": "node -e \"require('fs-extra').outputJsonSync('.git-tsconfig.json',{ extends: './tsconfig.json', include: ['*.d.ts', 'app/utils/i18n/*'].concat(process.env.StagedFiles.split(/\\n+/)) })\" && echo 'TS checking...\\n' && tsc -p .git-tsconfig.json --checkJs false"
19 | },
20 | "babel": {
21 | "presets": [
22 | "react-app"
23 | ],
24 | "plugins": [
25 | [
26 | "@babel/plugin-proposal-decorators",
27 | {
28 | "legacy": true
29 | }
30 | ]
31 | ]
32 | },
33 | "eslintConfig": {
34 | "extends": [
35 | "react-app",
36 | "./scripts/config/eslintrc.js"
37 | ]
38 | },
39 | "prettier": {
40 | "printWidth": 120,
41 | "tabWidth": 4,
42 | "parser": "babylon",
43 | "trailingComma": "none",
44 | "jsxBracketSameLine": true,
45 | "semi": true,
46 | "singleQuote": true,
47 | "overrides": [
48 | {
49 | "files": "*.json",
50 | "options": {
51 | "tabWidth": 2
52 | }
53 | }
54 | ]
55 | },
56 | "lint-staged": {
57 | "{app,static}/**/*.{js,jsx,mjs,css,scss,less,json,ts}": [
58 | "node_modules/.bin/prettier --write",
59 | "git add"
60 | ]
61 | },
62 | "dependencies": {
63 | "axios": "0.17.0",
64 | "bootstrap-sass": "3.4.1",
65 | "codemirror": "5.38.0",
66 | "jquery": "3.1.1",
67 | "lodash": "4.17.5",
68 | "normalize.css": "5.0.0",
69 | "prop-types": "15.7.2",
70 | "react-animated-router": "0.1.11",
71 | "react-awesome-snippets": "0.0.18",
72 | "react-router-dom": "5.2.0",
73 | "react-transition-group": "2.3.0"
74 | },
75 | "devDependencies": {
76 | "@babel/core": "7.8.6",
77 | "@babel/plugin-proposal-decorators": "7.8.3",
78 | "@commitlint/cli": "7.6.1",
79 | "@commitlint/config-conventional": "7.6.0",
80 | "@svgr/webpack": "4.3.3",
81 | "@types/react-router-dom": "5.1.3",
82 | "@types/react-transition-group": "4.2.4",
83 | "@typescript-eslint/eslint-plugin": "2.22.0",
84 | "@typescript-eslint/parser": "2.22.0",
85 | "ali-oss": "6.5.1",
86 | "autoprefixer": "7.1.6",
87 | "babel-loader": "8.0.6",
88 | "babel-plugin-named-asset-import": "0.3.6",
89 | "babel-runtime": "6.26.0",
90 | "case-sensitive-paths-webpack-plugin": "2.2.0",
91 | "chalk": "3.0.0",
92 | "check-dependencies": "1.1.0",
93 | "classlist-polyfill": "1.2.0",
94 | "core-js": "3.6.4",
95 | "css-loader": "3.4.2",
96 | "cz-conventional-changelog": "2.1.0",
97 | "detect-port": "1.2.1",
98 | "directory-named-webpack-plugin": "4.0.1",
99 | "dotenv": "8.2.0",
100 | "dotenv-expand": "5.1.0",
101 | "eslint": "7.14.0",
102 | "eslint-config-react-app": "6.0.0",
103 | "eslint-loader": "4.0.2",
104 | "eslint-plugin-flowtype": "4.6.0",
105 | "eslint-plugin-import": "2.22.1",
106 | "eslint-plugin-jsx-a11y": "6.4.1",
107 | "eslint-plugin-react": "7.21.5",
108 | "eslint-plugin-react-hooks": "2.5.0",
109 | "file-loader": "4.3.0",
110 | "fork-ts-checker-webpack-plugin-alt": "0.4.14",
111 | "fs-extra": "8.1.0",
112 | "glob": "7.1.6",
113 | "html-loader": "1.0.0-alpha.0",
114 | "html-webpack-plugin": "4.0.0-beta.11",
115 | "husky": "3.1.0",
116 | "i18next-scanner": "2.10.3",
117 | "imagemin-webpack-plugin": "2.4.2",
118 | "inline-chunk-manifest-html-webpack-plugin": "2.0.0",
119 | "less": "3.11.1",
120 | "less-loader": "5.0.0",
121 | "lint-staged": "9.5.0",
122 | "mini-css-extract-plugin": "0.9.0",
123 | "node-xlsx": "0.15.0",
124 | "optimize-css-assets-webpack-plugin": "5.0.3",
125 | "ora": "4.0.3",
126 | "postcss-flexbugs-fixes": "4.2.0",
127 | "postcss-loader": "3.0.0",
128 | "postcss-preset-env": "6.7.0",
129 | "postcss-safe-parser": "4.0.2",
130 | "prettier": "1.19.1",
131 | "raf-dom": "1.1.0",
132 | "raw-loader": "3.1.0",
133 | "react-dev-utils": "10.1.0",
134 | "rsync": "0.6.1",
135 | "sass": "1.26.2",
136 | "sass-loader": "8.0.2",
137 | "style-loader": "1.1.3",
138 | "stylelint": "12.0.1",
139 | "stylelint-config-recommended": "3.0.0",
140 | "sw-precache-webpack-plugin": "0.11.5",
141 | "terser-webpack-plugin": "2.3.5",
142 | "typescript": "4.9.5",
143 | "uglifyjs-webpack-plugin": "1.0.1",
144 | "webpack": "4.42.0",
145 | "webpack-dev-server": "3.10.3"
146 | },
147 | "husky": {
148 | "hooks": {
149 | "pre-commit": "lint-staged && export StagedFiles=$(git diff --name-only --diff-filter AM --relative --staged | grep -E '.tsx?$') && if [ -n \"$StagedFiles\" ]; then npm run tsc; fi",
150 | "pre-push": "CF=$(git diff --diff-filter AM --name-only @{u}..) || CF=$(git diff --diff-filter AM --name-only origin/master...HEAD); FILES=$(echo \"$CF\" | grep -E '^app/.*\\.m?[jt]sx?$'); if [ -n \"$FILES\" ]; then node_modules/.bin/eslint $FILES --max-warnings 0; fi"
151 | }
152 | },
153 | "stylelint": {
154 | "extends": "stylelint-config-recommended"
155 | },
156 | "browserslist": [
157 | ">0.2%",
158 | "not dead",
159 | "not op_mini all",
160 | "ie > 10"
161 | ],
162 | "config": {
163 | "commitizen": {
164 | "path": "cz-conventional-changelog"
165 | }
166 | },
167 | "engines": {
168 | "node": ">=8.10.0",
169 | "tiger-new": "4.3.10"
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/docs/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | react-formutil
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/docs/scripts/build.js:
--------------------------------------------------------------------------------
1 | /* eslint @typescript-eslint/no-var-requires: 0 */
2 |
3 | const useDevConfig = process.argv[2] === '--dev';
4 |
5 | process.env.BABEL_ENV = useDevConfig ? 'development' : 'production';
6 | process.env.NODE_ENV = useDevConfig ? 'development' : 'production';
7 |
8 | process.env.WEBPACK_BUILDING = 'true';
9 |
10 | process.on('unhandledRejection', err => {
11 | throw err;
12 | });
13 |
14 | require('./config/env');
15 |
16 | const chalk = require('chalk');
17 | const fs = require('fs-extra');
18 | const path = require('path');
19 | const webpack = require('webpack');
20 | const config = require(useDevConfig ? './config/webpack.config.dev' : './config/webpack.config.prod');
21 | const paths = require('./config/paths');
22 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
23 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
24 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
25 | const clearConsole = require('react-dev-utils/clearConsole');
26 | const measureFileSizesBeforeBuild = FileSizeReporter.measureFileSizesBeforeBuild;
27 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
28 | const checkMissDependencies = require('./config/checkMissDependencies');
29 | const { ensureLocals } = require('./i18n');
30 | const ora = require('ora');
31 |
32 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
33 | console.log();
34 | process.exit(1);
35 | }
36 |
37 | ensureLocals();
38 |
39 | const spinner = ora('webpack启动中...').start();
40 | // These sizes are pretty large. We'll warn for bundles exceeding them.
41 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
42 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
43 |
44 | checkMissDependencies(spinner)
45 | .then(() => {
46 | return measureFileSizesBeforeBuild(paths.appBuild);
47 | })
48 | .then(previousFileSizes => {
49 | // Remove all content but keep the directory
50 | fs.emptyDirSync(paths.appBuild);
51 | // Merge with the public folder
52 | copyPublicFolder();
53 | // Start the webpack build
54 | return build(previousFileSizes);
55 | })
56 | .then(({ stats, previousFileSizes, warnings }) => {
57 | if (warnings.length) {
58 | spinner.warn(chalk.yellow('编译有警告产生:'));
59 | console.log();
60 | console.log(warnings.join('\n\n'));
61 | console.log();
62 | // Teach some ESLint tricks.
63 | console.log('\n搜索相关' + chalk.underline(chalk.yellow('关键词')) + '以了解更多关于警告产生的原因.');
64 |
65 | console.log(
66 | '如果要忽略警告, 可以将 ' + chalk.cyan('// eslint-disable-next-line') + ' 添加到产生警告的代码行上方\n'
67 | );
68 |
69 | console.log();
70 | console.log();
71 |
72 | if (!useDevConfig) {
73 | spinner.fail(chalk.red('请处理所有的错误和警告后再build代码!'));
74 |
75 | console.log();
76 | console.log();
77 | process.exit(1);
78 | }
79 | } else {
80 | spinner.succeed(chalk.green('编译通过!!'));
81 | console.log();
82 | }
83 |
84 | spinner.succeed('gzip后的文件大小:');
85 | console.log();
86 |
87 | printFileSizesAfterBuild(
88 | stats,
89 | previousFileSizes,
90 | paths.appBuild,
91 | WARN_AFTER_BUNDLE_GZIP_SIZE,
92 | WARN_AFTER_CHUNK_GZIP_SIZE
93 | );
94 |
95 | console.log();
96 |
97 | if (/^http/.test(config.output.publicPath) === false) {
98 | spinner.succeed(chalk.green('项目打包完成,运行以下命令可即时预览:'));
99 | console.log();
100 |
101 | if (!paths.serve) {
102 | console.log(chalk.cyan('npm') + ' install -g serve');
103 | }
104 |
105 | console.log(chalk.cyan('serve') + ' -s ' + path.relative('.', paths.appBuild));
106 | } else {
107 | const publicPath = config.output.publicPath;
108 |
109 | spinner.succeed('项目打包完成,请确保资源已上传到:' + chalk.green(publicPath) + '.');
110 | }
111 |
112 | console.log();
113 | })
114 | .catch(err => {
115 | if (err) {
116 | spinner.fail(chalk.red('编译失败!!'));
117 | console.log();
118 | console.log(err.message || err);
119 | }
120 |
121 | console.log();
122 | console.log();
123 | process.exit(1);
124 | });
125 |
126 | // Create the production build and print the deployment instructions.
127 | function build(previousFileSizes) {
128 | let packText = useDevConfig ? '启动测试环境打包编译...' : '启动生产环境打包压缩...';
129 | let startTime = Date.now();
130 | let timer;
131 | let logProgress = function(stop) {
132 | var text = packText + '已耗时:' + ((Date.now() - startTime) / 1000).toFixed(3) + 's';
133 |
134 | if (stop) {
135 | clearTimeout(timer);
136 | spinner.succeed(chalk.green(text));
137 | } else {
138 | spinner.text = chalk.cyan(text);
139 |
140 | timer = setTimeout(logProgress, 100);
141 | }
142 | };
143 |
144 | clearConsole();
145 | logProgress();
146 |
147 | let compiler = webpack(config);
148 |
149 | return new Promise((resolve, reject) => {
150 | compiler.run((err, stats) => {
151 | let messages;
152 |
153 | logProgress(true); // 停止
154 | console.log();
155 |
156 | if (err) {
157 | if (!err.message) {
158 | return reject(err);
159 | }
160 |
161 | messages = formatWebpackMessages({
162 | errors: [err.message],
163 | warnings: []
164 | });
165 | } else {
166 | messages = formatWebpackMessages(stats.toJson({ all: false, warnings: true, errors: true }));
167 | }
168 |
169 | if (messages.errors.length) {
170 | if (messages.errors.length > 1) {
171 | messages.errors.length = 1;
172 | }
173 |
174 | return reject(new Error(messages.errors.join('\n\n')));
175 | }
176 |
177 | const resolveArgs = {
178 | stats,
179 | previousFileSizes,
180 | warnings: messages.warnings
181 | };
182 |
183 | return resolve(resolveArgs);
184 | });
185 | });
186 | }
187 |
188 | function copyPublicFolder() {
189 | fs.copySync(paths.appPublic, paths.appBuild, {
190 | dereference: true,
191 | filter: file => {
192 | const relative = path.relative(paths.appPublic, file);
193 | const basename = path.basename(file);
194 | const isDirectory = fs.statSync(file).isDirectory();
195 |
196 | return isDirectory
197 | ? basename !== 'layout' // layout目录不复制
198 | : !paths.pageEntries.find(name => name + '.html' === relative);
199 | }
200 | });
201 | }
202 |
--------------------------------------------------------------------------------
/docs/scripts/cdn.js:
--------------------------------------------------------------------------------
1 | /* eslint @typescript-eslint/no-var-requires: 0 */
2 | const path = require('path');
3 | const fs = require('fs-extra');
4 | const Rsync = require('rsync');
5 | const OSS = require('ali-oss');
6 | const chalk = require('chalk');
7 | const lodash = require('lodash');
8 | const glob = require('glob');
9 | const paths = require('./config/paths');
10 | const ora = require('ora');
11 | const pkg = require(paths.appPackageJson);
12 |
13 | const staticFileName = 'static.config.json';
14 | const staticConfigFile = path.resolve(paths.root, staticFileName);
15 | const oldStaticConfig = lodash.invert(getStaticConfig(staticConfigFile));
16 | const newStaticConfig = {};
17 |
18 | const spinner = ora();
19 | let throttleDelay = 0;
20 |
21 | if (process.env.SKIP_CDN === 'true') {
22 | spinner.info(chalk.cyan('本次构建忽略CDN任务'));
23 | } else if (pkg.cdn) {
24 | if ('server' in pkg.cdn || 'ali-oss' in pkg.cdn) {
25 | runCDN();
26 | } else {
27 | spinner.fail(chalk.red(`未发现CDN服务连接配置信息!`));
28 | spinner.fail(chalk.red(`如果不需要CDN服务,您可以移除 ${chalk.cyan('package.json')} 中的 cdn 字段;`));
29 | spinner.fail(chalk.red(`或者,请根据下述信息在 ${chalk.cyan('package.json')} 中补充相关配置:`));
30 |
31 | console.log(
32 | chalk.grey(`
33 | 支持两种cdn配置方式,分别需要在 ${chalk.cyan('package.json')} 中配置相关的cdn字段:
34 |
35 | {
36 | "name": "${pkg.name}",
37 | "version": "${pkg.version}",
38 | "cdn": {
39 | "host": "${pkg.cdn.host || 'https://xxx.com'}",
40 | "path": "${pkg.cdn.path || '/xxx'}",
41 | ${chalk.cyan(`"server": "host:path",
42 | "ali-oss": {
43 | ...
44 | }`)}
45 | },
46 | ...
47 | }
48 |
49 | server和ali-oss字段必选其一配置,别对对应下述两种cdn配置方式:
50 |
51 | 1. 阿里云的OSS存储服务,对应ali-oss配置(具体需要配置的内容可以参考阿里云文档)
52 | 2. 通过ssh的rsync命令传到源服务器上,对应server字段配置,即rsync命令的目标服务器与路径,例如:BEIJING_HOST:/data0/webservice/static
53 | `) // `
54 | );
55 | }
56 | } else {
57 | spinner.info(chalk.cyan('未发现CDN配置信息,已跳过'));
58 | }
59 |
60 | function getStaticConfig(path) {
61 | try {
62 | return require(path) || {};
63 | } catch (e) {
64 | return {};
65 | }
66 | }
67 |
68 | function removeFileNameHash(fileName) {
69 | const pipes = fileName.split('.');
70 |
71 | pipes.splice(-2, 1);
72 | return pipes.join('.');
73 | }
74 |
75 | function runCDN() {
76 | throttleDelay = 0;
77 |
78 | spinner.start('开始上传');
79 |
80 | let exitsNum = 0;
81 | const useOSS = !!pkg.cdn['ali-oss'];
82 | const allFiles = glob.sync(path.join(paths.appBuild, 'static/**/!(*.map)'));
83 | const allSyncPromises = allFiles
84 | .filter(function(file) {
85 | const relative = path.relative(paths.appBuild, file);
86 |
87 | // 文件夹不处理
88 | if (fs.statSync(file).isDirectory()) {
89 | return false;
90 | }
91 |
92 | newStaticConfig[/js$|css$/.test(relative) ? removeFileNameHash(relative) : relative] = relative;
93 |
94 | // 已经存在
95 | if (oldStaticConfig[relative]) {
96 | spinner.succeed(chalk.green('已存在:' + relative));
97 | exitsNum++;
98 | return false;
99 | }
100 |
101 | return true;
102 | })
103 | .map(useOSS ? createOSS : createRsync);
104 |
105 | Promise.all(allSyncPromises).then(function(rets) {
106 | let uploadNum = rets.filter(Boolean).length;
107 | let failNum = rets.length - uploadNum;
108 |
109 | console.log();
110 |
111 | console.log(
112 | chalk[failNum ? 'red' : 'cyan'](
113 | '+++++++++++++++++++++++++++++++\n 文件上传完毕(' +
114 | chalk.blue(pkg.cdn.path) +
115 | ') \n ' +
116 | chalk.magenta('成功: ' + uploadNum) +
117 | ' \n ' +
118 | chalk.red('失败: ' + failNum) +
119 | ' \n ' +
120 | chalk.green('重复: ' + exitsNum) +
121 | '\n+++++++++++++++++++++++++++++++'
122 | )
123 | );
124 |
125 | if (!failNum) {
126 | fs.outputFileSync(staticConfigFile, JSON.stringify(newStaticConfig, '\n', 2));
127 | console.log(chalk.blue('配置文件已经更新: ' + staticConfigFile));
128 | console.log();
129 | console.log(chalk.green('项目已经成功编译,运行以下命令可即时预览:'));
130 |
131 | if (!paths.serve) {
132 | console.log(chalk.cyan('npm') + ' install -g serve');
133 | }
134 |
135 | console.log(chalk.cyan('serve') + ' -s ' + path.relative('.', paths.appBuild));
136 | } else {
137 | console.log(chalk.red('文件未全部上传,请单独运行') + chalk.green(' npm run cdn ') + chalk.red('命令!'));
138 | process.exit(1);
139 | }
140 |
141 | console.log();
142 | });
143 | }
144 |
145 | function createRsync(file) {
146 | return new Promise(resolve => {
147 | setTimeout(() => {
148 | const rsync = new Rsync();
149 | const relative = path.relative(paths.appBuild, file);
150 |
151 | rsync.cwd(paths.appBuild);
152 |
153 | rsync
154 | .flags('Rz') // 相对路径上传、压缩
155 | .source(relative)
156 | .destination(path.join(pkg.cdn.server || 'static:/data0/webservice/static', pkg.cdn.path))
157 | .execute(function(error, code, cmd) {
158 | if (error) {
159 | resolve(false);
160 | spinner.fail(chalk.red('上传失败(' + error + '):' + relative));
161 | } else {
162 | resolve(true);
163 | spinner.warn(chalk.yellow('已上传:' + relative));
164 | }
165 | });
166 | }, 200 * throttleDelay++);
167 | });
168 | }
169 |
170 | function createOSS(file) {
171 | return new Promise(resolve => {
172 | setTimeout(() => {
173 | const client = new OSS(pkg.cdn['ali-oss']);
174 | const objectName = path.relative(paths.appBuild, file);
175 |
176 | client
177 | .put(path.join(pkg.cdn.path, objectName), file)
178 | .then(() => {
179 | resolve(true);
180 | spinner.warn(chalk.yellow('已上传:' + objectName));
181 | })
182 | .catch(error => {
183 | resolve(false);
184 | spinner.fail(chalk.red('上传失败(' + error + '):' + objectName));
185 | });
186 | }, 200 * throttleDelay++);
187 | });
188 | }
189 |
--------------------------------------------------------------------------------
/docs/scripts/config/checkMissDependencies.js:
--------------------------------------------------------------------------------
1 | /* eslint @typescript-eslint/no-var-requires: 0 */
2 | const checkDependencies = require('check-dependencies');
3 | const inquirer = require('react-dev-utils/inquirer');
4 | const spawn = require('cross-spawn');
5 | const chalk = require('chalk');
6 | const paths = require('./paths');
7 |
8 | async function checkMissDeps(spinner) {
9 | const result = await checkDependencies({
10 | packageDir: paths.root
11 | });
12 |
13 | if (result.status !== 0) {
14 | spinner.stop();
15 |
16 | // 输出错误信息
17 | result.error.forEach(function(err) {
18 | console.log(err);
19 | });
20 |
21 | console.log();
22 |
23 | const { reInstall } = await inquirer.prompt([
24 | {
25 | name: 'reInstall',
26 | type: 'confirm',
27 | message:
28 | '你当前安装的依赖版本和要求的不一致,是否要重新安装所有依赖?\n' +
29 | chalk.dim('重新运行 npm install 安装所有依赖项.'),
30 | default: true
31 | }
32 | ]);
33 |
34 | console.log();
35 |
36 | if (reInstall) {
37 | await new Promise((resolve, reject) => {
38 | install(function(code, command, args) {
39 | if (code !== 0) {
40 | spinner.fail('`' + command + ' ' + args.join(' ') + '` 运行失败');
41 |
42 | reject();
43 | } else {
44 | resolve();
45 | }
46 | });
47 | });
48 |
49 | spinner.succeed(chalk.green('项目依赖已更新'));
50 | } else {
51 | spinner.warn(chalk.yellow('你需要按照下面命令操作后才能继续:'));
52 | console.log();
53 |
54 | console.log(chalk.green(' ' + paths.npmCommander + ' install'));
55 |
56 | return Promise.reject();
57 | }
58 | }
59 | }
60 |
61 | function install(callback) {
62 | let command = paths.npmCommander;
63 | let args = ['install'];
64 |
65 | var child = spawn(command, args, {
66 | stdio: 'inherit'
67 | });
68 |
69 | child.on('close', function(code) {
70 | callback(code, command, args);
71 | });
72 | }
73 |
74 | module.exports = checkMissDeps;
75 |
--------------------------------------------------------------------------------
/docs/scripts/config/env.js:
--------------------------------------------------------------------------------
1 | /* eslint @typescript-eslint/no-var-requires: 0 */
2 | const fs = require('fs');
3 | const path = require('path');
4 | const paths = require('./paths');
5 | const pkg = require(paths.appPackageJson);
6 |
7 | delete require.cache[require.resolve('./paths')];
8 |
9 | const NODE_ENV = process.env.NODE_ENV;
10 |
11 | let dotenvFiles = [
12 | `${paths.dotenv}.${NODE_ENV}.local`,
13 | `${paths.dotenv}.${NODE_ENV}`,
14 | NODE_ENV !== 'test' && `${paths.dotenv}.local`,
15 | paths.dotenv
16 | ].filter(Boolean);
17 |
18 | dotenvFiles.forEach(dotenvFile => {
19 | if (fs.existsSync(dotenvFile)) {
20 | require('dotenv-expand')(
21 | require('dotenv').config({
22 | path: dotenvFile
23 | })
24 | );
25 | }
26 | });
27 |
28 | const appDirectory = fs.realpathSync(process.cwd());
29 |
30 | process.env.NODE_PATH = (process.env.NODE_PATH || '')
31 | .split(path.delimiter)
32 | .filter(folder => folder && !path.isAbsolute(folder))
33 | .map(folder => path.resolve(appDirectory, folder))
34 | .join(path.delimiter);
35 |
36 | if (!('BASE_NAME' in process.env) && 'basename' in pkg) {
37 | process.env.BASE_NAME = pkg.basename;
38 | }
39 |
40 | const REACT_APP = /^(REACT_APP_|TIGER_)/i;
41 | const whitelists = ['BASE_NAME'];
42 |
43 | function getClientEnvironment(publicUrl) {
44 | const raw = Object.keys(process.env)
45 | .filter(key => REACT_APP.test(key) || whitelists.includes(key))
46 | .reduce(
47 | (env, key) => {
48 | env[key] = process.env[key];
49 | return env;
50 | },
51 | {
52 | NODE_ENV: process.env.NODE_ENV || 'development',
53 | PUBLIC_URL: publicUrl
54 | }
55 | );
56 | // Stringify all values so we can feed into Webpack DefinePlugin
57 | const stringified = {
58 | 'process.env': Object.keys(raw).reduce((env, key) => {
59 | env[key] = JSON.stringify(raw[key]);
60 | return env;
61 | }, {})
62 | };
63 |
64 | return { raw, stringified };
65 | }
66 |
67 | module.exports = getClientEnvironment;
68 |
--------------------------------------------------------------------------------
/docs/scripts/config/eslintrc.js:
--------------------------------------------------------------------------------
1 | const paths = require('./paths');
2 |
3 | /**
4 | * 0: off
5 | * 1: warn
6 | * 2: error
7 | */
8 | module.exports = {
9 | overrides: [
10 | {
11 | files: ['**/*.ts?(x)'],
12 | /* parserOptions: {
13 | * project: paths.appTsConfig,
14 | * }, */
15 | rules: {
16 | // typescript
17 | '@typescript-eslint/no-use-before-define': [2, { functions: false, classes: false }],
18 | '@typescript-eslint/unified-signatures': 1,
19 | // '@typescript-eslint/await-thenable': 2,
20 | '@typescript-eslint/camelcase': 0,
21 | 'no-unused-vars': 0,
22 | '@typescript-eslint/no-unused-vars': [
23 | 1,
24 | {
25 | vars: 'all',
26 | args: 'after-used',
27 | ignoreRestSiblings: true,
28 | varsIgnorePattern: '^_',
29 | argsIgnorePattern: '^_|^err|^ev' // _xxx, err, error, ev, event
30 | }
31 | ],
32 | '@typescript-eslint/adjacent-overload-signatures': 2,
33 | '@typescript-eslint/array-type': [
34 | 1,
35 | {
36 | default: 'array-simple'
37 | }
38 | ],
39 | '@typescript-eslint/ban-types': 2,
40 | '@typescript-eslint/class-name-casing': 2,
41 | '@typescript-eslint/explicit-function-return-type': 0,
42 | '@typescript-eslint/explicit-member-accessibility': 0,
43 | '@typescript-eslint/interface-name-prefix': 0,
44 | '@typescript-eslint/member-delimiter-style': 2,
45 | '@typescript-eslint/no-empty-interface': 1,
46 | '@typescript-eslint/no-extra-non-null-assertion': 2,
47 | '@typescript-eslint/no-explicit-any': 0,
48 | '@typescript-eslint/no-inferrable-types': 0,
49 | '@typescript-eslint/no-misused-new': 2,
50 | '@typescript-eslint/no-non-null-assertion': 0,
51 | '@typescript-eslint/consistent-type-assertions': 2,
52 | '@typescript-eslint/no-parameter-properties': 2,
53 | '@typescript-eslint/triple-slash-reference': 2,
54 | '@typescript-eslint/no-var-requires': 2,
55 | '@typescript-eslint/consistent-type-definitions': [2, 'interface'],
56 | '@typescript-eslint/no-namespace': 2,
57 | '@typescript-eslint/prefer-namespace-keyword': 2,
58 | '@typescript-eslint/type-annotation-spacing': 1
59 | }
60 | }
61 | ],
62 | globals: {
63 | __: true
64 | },
65 | parserOptions: {
66 | ecmaFeatures: {
67 | legacyDecorators: true
68 | }
69 | },
70 | rules: {
71 | 'react/jsx-no-target-blank': 0,
72 | 'react/no-unsafe': [2, { checkAliases: true }],
73 | 'react/no-deprecated': 2,
74 | 'react/no-string-refs': [1, { noTemplateLiterals: true }],
75 | 'jsx-a11y/anchor-is-valid': 0,
76 | 'import/no-anonymous-default-export': [
77 | 2,
78 | {
79 | allowArray: true,
80 | allowArrowFunction: false,
81 | allowAnonymousClass: false,
82 | allowAnonymousFunction: false,
83 | allowCallExpression: true, // The true value here is for backward compatibility
84 | allowLiteral: true,
85 | allowObject: true
86 | }
87 | ],
88 | eqeqeq: [1, 'smart'],
89 | radix: 0,
90 | 'no-script-url': 0,
91 | 'linebreak-style': [1, 'unix'],
92 | indent: 0, // process by prettier
93 | semi: 0, // process by prettier
94 | 'semi-spacing': [1, { before: false }],
95 | 'no-extra-semi': 1,
96 | 'padded-blocks': [1, 'never'],
97 | 'one-var-declaration-per-line': [1, 'initializations'],
98 | 'spaced-comment': [1, 'always'],
99 | 'space-in-parens': [1, 'never'],
100 | 'space-before-function-paren': [
101 | 1,
102 | {
103 | anonymous: 'never',
104 | named: 'never',
105 | asyncArrow: 'always'
106 | }
107 | ],
108 | 'space-unary-ops': 1,
109 | 'space-infix-ops': 1,
110 | 'space-before-blocks': 1,
111 | 'no-trailing-spaces': [1, { ignoreComments: true }],
112 | 'key-spacing': [1, { mode: 'strict' }],
113 | 'switch-colon-spacing': 1,
114 | 'func-call-spacing': [1, 'never'],
115 | 'keyword-spacing': 1,
116 | 'no-multiple-empty-lines': [
117 | 1,
118 | {
119 | max: 2,
120 | maxEOF: 1,
121 | maxBOF: 1
122 | }
123 | ],
124 | 'default-case': [1, { commentPattern: '^no[-\\s]+default$' }],
125 | curly: 2,
126 | 'dot-notation': 1,
127 | 'no-else-return': 2,
128 | 'guard-for-in': 2,
129 | 'no-empty-pattern': 2,
130 | 'no-implied-eval': 2,
131 | 'no-global-assign': 2,
132 | 'no-multi-spaces': [
133 | 1,
134 | {
135 | ignoreEOLComments: true,
136 | exceptions: {
137 | VariableDeclarator: true,
138 | ImportDeclaration: true
139 | }
140 | }
141 | ],
142 | 'no-lone-blocks': 2,
143 | 'no-self-compare': 2,
144 | 'no-sequences': 2,
145 | yoda: 1,
146 | 'no-unexpected-multiline': 1,
147 | 'no-with': 2,
148 | 'no-useless-escape': 2,
149 | 'no-useless-concat': 2,
150 | 'no-unused-vars': [
151 | 1,
152 | {
153 | vars: 'all',
154 | args: 'after-used',
155 | ignoreRestSiblings: true,
156 | varsIgnorePattern: '^_',
157 | argsIgnorePattern: '^_|^err|^ev' // _xxx, err, error, ev, event
158 | }
159 | ],
160 | 'no-unmodified-loop-condition': 2,
161 | 'wrap-iife': [2, 'inside'],
162 | 'lines-between-class-members': [1, 'always', { exceptAfterSingleLine: true }],
163 | 'padding-line-between-statements': [
164 | 1,
165 | {
166 | blankLine: 'always',
167 | prev: [
168 | 'multiline-block-like',
169 | 'multiline-expression',
170 | 'const',
171 | 'let',
172 | 'var',
173 | 'cjs-import',
174 | 'import',
175 | 'export',
176 | 'cjs-export',
177 | 'class',
178 | 'throw',
179 | 'directive'
180 | ],
181 | next: '*'
182 | },
183 | {
184 | blankLine: 'always',
185 | prev: '*',
186 | next: [
187 | 'multiline-block-like',
188 | 'multiline-expression',
189 | 'const',
190 | 'let',
191 | 'var',
192 | 'cjs-import',
193 | 'import',
194 | 'export',
195 | 'cjs-export',
196 | 'class',
197 | 'throw',
198 | 'return'
199 | ]
200 | },
201 | { blankLine: 'any', prev: ['cjs-import', 'import'], next: ['cjs-import', 'import'] },
202 | { blankLine: 'any', prev: ['export', 'cjs-export'], next: ['export', 'cjs-export'] },
203 | { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] }
204 | ]
205 | }
206 | };
207 |
--------------------------------------------------------------------------------
/docs/scripts/config/helper.js:
--------------------------------------------------------------------------------
1 | /* eslint @typescript-eslint/no-var-requires: 0 */
2 | const getProcessForPort = require('react-dev-utils/getProcessForPort');
3 | const clearConsole = require('react-dev-utils/clearConsole');
4 | const detect = require('detect-port-alt');
5 | const inquirer = require('react-dev-utils/inquirer');
6 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
7 | const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware');
8 | const evalSourceMapMiddleware = require('react-dev-utils/evalSourceMapMiddleware');
9 | const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware');
10 | const forkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin');
11 | const ignoredFiles = require('react-dev-utils/ignoredFiles');
12 | const typescriptFormatter = require('react-dev-utils/typescriptFormatter');
13 | const config = require('./webpack.config.dev');
14 | const paths = require('./paths');
15 | const chalk = require('chalk');
16 | const fs = require('fs');
17 | const path = require('path');
18 | const pkg = require(paths.appPackageJson);
19 |
20 | const isInteractive = process.stdout.isTTY;
21 |
22 | function choosePort(host, defaultPort, spinner) {
23 | return detect(defaultPort, host).then(
24 | port =>
25 | new Promise(resolve => {
26 | if (port === defaultPort) {
27 | return resolve(port);
28 | }
29 |
30 | spinner.stop();
31 | clearConsole();
32 |
33 | const existingProcess = getProcessForPort(defaultPort);
34 | const question = {
35 | type: 'confirm',
36 | name: 'shouldChangePort',
37 | message:
38 | '端口(' +
39 | chalk.yellow(defaultPort) +
40 | ')被占用,可能的程序是: \n ' +
41 | existingProcess +
42 | '\n' +
43 | ' 要换一个端口运行本程序吗?',
44 | default: true
45 | };
46 |
47 | inquirer.prompt(question).then(answer => {
48 | if (answer.shouldChangePort) {
49 | resolve(port);
50 | } else {
51 | resolve(null);
52 | }
53 | });
54 | }),
55 | err => {
56 | throw new Error(
57 | chalk.red(`无法为 ${chalk.bold(host)} 找到可用的端口.`) +
58 | '\n' +
59 | ('错误信息: ' + err.message || err) +
60 | '\n'
61 | );
62 | }
63 | );
64 | }
65 |
66 | function createCompiler(webpack, config, appName, urls, devSocket, spinner) {
67 | let compiler;
68 | let stime = Date.now();
69 |
70 | try {
71 | compiler = webpack(config);
72 | } catch (err) {
73 | console.log(chalk.red('启动编译失败!'));
74 | console.log();
75 | console.log(err.message || err);
76 | console.log();
77 | process.exit(1);
78 | }
79 |
80 | compiler.hooks.invalid.tap('invalid', () => {
81 | if (isInteractive) {
82 | clearConsole();
83 | }
84 |
85 | stime = Date.now();
86 | spinner.text = chalk.cyan('重新编译...');
87 | });
88 |
89 | let isFirstCompile = true;
90 | let tsMessagesPromise;
91 | let tsMessagesResolver;
92 |
93 | compiler.hooks.beforeCompile.tap('beforeCompile', () => {
94 | tsMessagesPromise = new Promise(resolve => {
95 | tsMessagesResolver = msgs => resolve(msgs);
96 | });
97 | });
98 |
99 | forkTsCheckerWebpackPlugin.getCompilerHooks(compiler).receive.tap('afterTypeScriptCheck', (diagnostics, lints) => {
100 | const allMsgs = [...diagnostics, ...lints];
101 | const format = message => `${message.file}\n${typescriptFormatter(message, true)}`;
102 |
103 | tsMessagesResolver({
104 | errors: allMsgs.filter(msg => msg.severity === 'error').map(format),
105 | warnings: allMsgs.filter(msg => msg.severity === 'warning').map(format)
106 | });
107 | });
108 |
109 | // "done" event fires when Webpack has finished recompiling the bundle.
110 | // Whether or not you have warnings or errors, you will get this event.
111 | compiler.hooks.done.tap('done', async stats => {
112 | if (isInteractive) {
113 | clearConsole();
114 | }
115 |
116 | const useTimer = (isTotal = false) =>
117 | chalk.grey(`(编译${isTotal ? '总' : '已'}耗时: ${(Date.now() - stime) / 1000}s)`);
118 |
119 | const statsData = stats.toJson({
120 | all: false,
121 | warnings: true,
122 | errors: true
123 | });
124 |
125 | if (statsData.errors.length === 0) {
126 | const delayedMsg = setTimeout(() => {
127 | spinner.text = chalk.cyan('文件已编译,正在TSC检查...') + useTimer();
128 | }, 100);
129 |
130 | const messages = await tsMessagesPromise;
131 |
132 | clearTimeout(delayedMsg);
133 | statsData.errors.push(...messages.errors);
134 | statsData.warnings.push(...messages.warnings);
135 |
136 | stats.compilation.errors.push(...messages.errors);
137 | stats.compilation.warnings.push(...messages.warnings);
138 |
139 | if (messages.errors.length > 0) {
140 | devSocket.errors(messages.errors);
141 | } else if (messages.warnings.length > 0) {
142 | devSocket.warnings(messages.warnings);
143 | }
144 |
145 | if (isInteractive) {
146 | clearConsole();
147 | }
148 | }
149 |
150 | const messages = formatWebpackMessages(statsData);
151 | const isSuccessful = !messages.errors.length && !messages.warnings.length;
152 |
153 | if (isSuccessful && (isInteractive || isFirstCompile)) {
154 | spinner.succeed(chalk.green('编译通过!' + useTimer(true)));
155 | console.log();
156 | spinner.succeed(chalk.green('应用(' + appName + ')已启动:'));
157 | console.log();
158 |
159 | if (urls.lanUrlForTerminal) {
160 | console.log(` ${chalk.bold('本地:')} ${chalk.cyan(urls.localUrlForTerminal)}`);
161 | console.log(` ${chalk.bold('远程:')} ${chalk.cyan(urls.lanUrlForTerminal)}`);
162 | } else {
163 | console.log(` ${urls.localUrlForTerminal}`);
164 | }
165 | }
166 |
167 | isFirstCompile = false;
168 |
169 | // If errors exist, only show errors.
170 | if (messages.errors.length) {
171 | if (messages.errors.length > 1) {
172 | messages.errors.length = 1;
173 | }
174 |
175 | spinner.fail(chalk.red('编译失败!!' + useTimer(true)));
176 | console.log();
177 | console.log(messages.errors.join('\n\n'));
178 | console.log();
179 | }
180 |
181 | // Show warnings if no errors were found.
182 | if (messages.warnings.length) {
183 | spinner.warn(chalk.yellow('编译有警告产生:' + useTimer(true)));
184 | console.log();
185 | console.log(messages.warnings.join('\n\n'));
186 | console.log();
187 |
188 | // Teach some ESLint tricks.
189 | console.log('\n搜索相关' + chalk.underline(chalk.yellow('关键词')) + '以了解更多关于警告产生的原因.');
190 |
191 | console.log(
192 | '如果要忽略警告, 可以将 ' + chalk.cyan('// eslint-disable-next-line') + ' 添加到产生警告的代码行上方\n'
193 | );
194 | }
195 |
196 | console.log();
197 | spinner.text = chalk.cyan('webpack运行中...');
198 | spinner.render().start();
199 | });
200 |
201 | return compiler;
202 | }
203 |
204 | function createDevServerConfig(proxy, allowedHost) {
205 | return {
206 | headers: {
207 | 'Access-Control-Allow-Origin': '*',
208 | 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, HEAD, DELETE, FETCH'
209 | },
210 | disableHostCheck: !proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true',
211 | // Enable gzip
212 | compress: true,
213 | clientLogLevel: 'none',
214 | contentBase: paths.appPublic,
215 | watchContentBase: true,
216 | hot: true,
217 | transportMode: 'ws',
218 | injectClient: false,
219 | publicPath: config.output.publicPath,
220 | quiet: true,
221 | watchOptions: {
222 | ignored: ignoredFiles(paths.appSrc)
223 | },
224 | https: process.env.HTTPS === 'true',
225 | host: process.env.HOST || '0.0.0.0',
226 | overlay: false,
227 | historyApiFallback: pkg.noRewrite
228 | ? false
229 | : {
230 | disableDotRule: true
231 | },
232 | public: allowedHost,
233 | proxy,
234 | before(app, server) {
235 | if (fs.existsSync(paths.proxySetup)) {
236 | require(paths.proxySetup)(app);
237 | }
238 |
239 | app.use(evalSourceMapMiddleware(server));
240 | app.use(errorOverlayMiddleware());
241 | app.use(noopServiceWorkerMiddleware());
242 | }
243 | };
244 | }
245 |
246 | function onProxyError(proxy) {
247 | return function(err, req, res) {
248 | var host = req.headers && req.headers.host;
249 |
250 | console.log(
251 | chalk.red('代理错误:') +
252 | '无法将 ' +
253 | chalk.cyan(req.url) +
254 | ' 的请求从 ' +
255 | chalk.cyan(host) +
256 | ' 转发到 ' +
257 | chalk.cyan(proxy) +
258 | '.'
259 | );
260 |
261 | console.log(
262 | '点击 https://nodejs.org/api/errors.html#errors_common_system_errors 查看更多信息 (' +
263 | chalk.cyan(err.code) +
264 | ').'
265 | );
266 |
267 | console.log();
268 |
269 | if (res.writeHead && !res.headersSent) {
270 | res.writeHead(500);
271 | }
272 |
273 | res.end('代理错误: 无法将 ' + req.url + ' 的请求从 ' + host + ' 转发到 ' + proxy + ' (' + err.code + ').');
274 | };
275 | }
276 |
277 | function mayProxy(pathname) {
278 | const maybePublicPath = path.resolve(paths.appPublic, pathname.slice(1));
279 |
280 | return !fs.existsSync(maybePublicPath);
281 | }
282 |
283 | function prepareProxy(proxy) {
284 | if (!proxy) {
285 | return undefined;
286 | }
287 |
288 | if (typeof proxy === 'object') {
289 | return Object.keys(proxy).map(function(path) {
290 | const opt =
291 | typeof proxy[path] === 'object'
292 | ? proxy[path]
293 | : {
294 | target: proxy[path]
295 | };
296 | const target = opt.target;
297 |
298 | return Object.assign({}, opt, {
299 | context: function(pathname) {
300 | return mayProxy(pathname) && pathname.match(path);
301 | },
302 | onProxyReq: proxyReq => {
303 | if (proxyReq.getHeader('origin')) {
304 | proxyReq.setHeader('origin', target);
305 | }
306 | },
307 | onError: onProxyError(target)
308 | });
309 | });
310 | }
311 |
312 | if (!/^http(s)?:\/\//.test(proxy)) {
313 | console.log(chalk.red('proxy 只能是一个 http:// 或者 https:// 开头的字符串或者一个object配置'));
314 | console.log(chalk.red('当前 proxy 的类型是 "' + typeof proxy + '"。'));
315 | console.log(chalk.red('你可以从 package.json 中移除它,或者设置一个字符串地址(目标服务器)'));
316 | process.exit(1);
317 | }
318 |
319 | return [
320 | {
321 | target: proxy,
322 | logLevel: 'silent',
323 | context: function(pathname, req) {
324 | return (
325 | req.method !== 'GET' ||
326 | (mayProxy(pathname) && req.headers.accept && req.headers.accept.indexOf('text/html') === -1)
327 | );
328 | },
329 | onProxyReq: proxyReq => {
330 | if (proxyReq.getHeader('origin')) {
331 | proxyReq.setHeader('origin', proxy);
332 | }
333 | },
334 | onError: onProxyError(proxy),
335 | secure: false,
336 | changeOrigin: true,
337 | ws: true,
338 | xfwd: true
339 | }
340 | ];
341 | }
342 |
343 | module.exports = {
344 | choosePort,
345 | prepareProxy,
346 | createCompiler,
347 | createDevServerConfig
348 | };
349 |
--------------------------------------------------------------------------------
/docs/scripts/config/paths.js:
--------------------------------------------------------------------------------
1 | /* eslint @typescript-eslint/no-var-requires: 0 */
2 | const path = require('path');
3 | const fs = require('fs-extra');
4 | const glob = require('glob');
5 | const execSync = require('child_process').execSync;
6 | const isDev = process.env.NODE_ENV === 'development';
7 | const lodash = require('lodash');
8 |
9 | // Make sure any symlinks in the project folder are resolved:
10 | // https://github.com/facebookincubator/create-react-app/issues/637
11 | const appDirectory = fs.realpathSync(process.cwd());
12 | const pkg = require(resolveApp('package.json'));
13 |
14 | function resolveApp(relativePath) {
15 | return path.resolve(appDirectory, relativePath);
16 | }
17 |
18 | const nodePaths = (process.env.NODE_PATH || '')
19 | .split(path.delimiter)
20 | .filter(Boolean)
21 | .map(resolveApp);
22 |
23 | const entries = {};
24 |
25 | glob.sync(resolveApp('app/!(_)*.{j,t}s?(x)')).forEach(function(file) {
26 | const basename = path.basename(file).replace(/\.[jt]sx?$/, '');
27 |
28 | entries[basename] = file;
29 | });
30 |
31 | const alias = Object.assign(
32 | {
33 | components: resolveApp('app/components'),
34 | modules: resolveApp('app/modules'),
35 | utils: resolveApp('app/utils'),
36 | stores: resolveApp('app/stores'),
37 | types: resolveApp('app/types'),
38 | hooks: resolveApp('app/hooks')
39 | },
40 | lodash.mapValues(pkg.alias, function(relativePath) {
41 | if (fs.pathExistsSync(resolveApp(relativePath))) {
42 | return resolveApp(relativePath);
43 | }
44 |
45 | return relativePath;
46 | })
47 | );
48 |
49 | // config after eject: we're in ./config/
50 | module.exports = {
51 | dotenv: resolveApp('.env'),
52 | root: resolveApp(''),
53 | appBuild: resolveApp(process.env.BUILD_DIR || 'demo'),
54 | appPublic: resolveApp('public'),
55 | appHtml: resolveApp('public/index.html'),
56 | appIndexJs: Object.values(entries)[0] || resolveApp('app/index.js'),
57 | appPackageJson: resolveApp('package.json'),
58 | appSrc: resolveApp('app'),
59 | formutilSrc: resolveApp('app/../../src'),
60 | appTsConfig: resolveApp('tsconfig.json'),
61 | staticSrc: resolveApp('static'),
62 | locals: resolveApp('locals'),
63 | proxySetup: resolveApp('setupProxy.js'),
64 | appNodeModules: resolveApp('node_modules'),
65 | ownNodeModules: resolveApp('node_modules'),
66 | nodePaths: nodePaths,
67 | alias: alias,
68 | entries: entries,
69 | pageEntries: glob.sync(resolveApp('public/!(_)*.html')).map(function(file) {
70 | return path.basename(file, '.html');
71 | }),
72 | moduleFileExtensions: ['.js', '.json', '.jsx', '.mjs', '.ts', '.tsx'],
73 | // 一些命令检测
74 | serve: hasInstall('serve'),
75 | npmCommander: ['tnpm', 'cnpm', 'npm'].find(hasInstall)
76 | };
77 |
78 | function hasInstall(command) {
79 | try {
80 | execSync(command + ' --version', {
81 | stdio: 'ignore'
82 | });
83 |
84 | return true;
85 | } catch (e) {
86 | return false;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/docs/scripts/config/polyfills.js:
--------------------------------------------------------------------------------
1 | /* eslint @typescript-eslint/no-var-requires: 0 */
2 | // classList
3 | require('classlist-polyfill');
4 |
5 | // requestAnimationFrame
6 | require('raf-dom').polyfill();
7 |
8 | // ECMAScript
9 | require('core-js/features/object');
10 | require('core-js/features/promise');
11 | require('core-js/features/map');
12 | require('core-js/features/set');
13 | require('core-js/features/array');
14 | require('core-js/features/string');
15 | require('core-js/features/number');
16 | require('core-js/features/symbol');
17 |
18 | // require('core-js/features/url');
19 | // require('core-js/features/url-search-params');
20 |
--------------------------------------------------------------------------------
/docs/scripts/config/tslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "trailing-comma": false,
4 | "quotemark": false,
5 | "no-console": false,
6 | "semicolon": false,
7 | "no-namespace": [true, "allow-declarations"],
8 | "max-classes-per-file": false,
9 | "ordered-imports": false,
10 | "member-ordering": false,
11 | "object-literal-sort-keys": false,
12 | "member-access": false,
13 | "arrow-parens": [true, "ban-single-arg-parens"],
14 | "variable-name": false,
15 | "prefer-const": false,
16 | "jsx-boolean-value": false,
17 | "only-arrow-functions": false,
18 | "jsx-no-lambda": false,
19 | "interface-name": false
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/docs/scripts/config/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | /* eslint @typescript-eslint/no-var-requires: 0 */
2 | const fs = require('fs');
3 | const path = require('path');
4 | const resolve = require('resolve');
5 | const webpack = require('webpack');
6 | const HtmlWebpackPlugin = require('html-webpack-plugin');
7 | const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
8 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
9 | const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
10 | const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
11 | const DirectoryNamedWebpackPlugin = require('directory-named-webpack-plugin');
12 | const getClientEnvironment = require('./env');
13 | const paths = require('./paths');
14 | const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
15 | const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin');
16 | const pkg = require(paths.appPackageJson);
17 | const isBuilding = process.env.WEBPACK_BUILDING === 'true';
18 |
19 | const publicPath = isBuilding ? path.join(pkg.noRewrite ? '.' : process.env.BASE_NAME || '', '/') : '/';
20 | const publicUrl = publicPath.slice(0, -1);
21 | const env = getClientEnvironment(publicUrl);
22 | const injects = [];
23 |
24 | // eslint-disable-next-line
25 | const matchScriptStylePattern = /<\!--\s*script:\s*([\w]+)(?:\.[jt]sx?)?\s*-->/g;
26 | const hotDev = require.resolve('react-dev-utils/webpackHotDevClient');
27 | const babelOption = {
28 | babelrc: false,
29 | configFile: false,
30 | compact: false,
31 | presets: [[require.resolve('babel-preset-react-app/dependencies'), { helpers: true }]],
32 | cacheDirectory: true,
33 | cacheCompression: false,
34 | sourceMaps: false
35 | };
36 |
37 | paths.pageEntries.forEach(function(name) {
38 | var chunks = ['_vendor_'];
39 | var file = path.resolve(paths.appPublic, name + '.html');
40 |
41 | if (paths.entries[name]) {
42 | chunks.push(name);
43 | }
44 |
45 | var contents = fs.readFileSync(file);
46 | var matches;
47 |
48 | while ((matches = matchScriptStylePattern.exec(contents))) {
49 | chunks.push(matches[1]);
50 | }
51 |
52 | injects.push(
53 | new HtmlWebpackPlugin({
54 | chunks: chunks,
55 | filename: name + '.html',
56 | template: file,
57 | inject: true,
58 | chunksSortMode: 'manual'
59 | })
60 | );
61 | });
62 |
63 | module.exports = {
64 | mode: 'development',
65 | devtool: 'cheap-module-source-map',
66 | entry: Object.assign(
67 | {
68 | _vendor_: [require.resolve('./polyfills'), !isBuilding && hotDev].filter(Boolean).concat(pkg.vendor || [])
69 | },
70 | paths.entries
71 | ),
72 | output: {
73 | path: paths.appBuild,
74 | pathinfo: true,
75 | filename: 'static/js/[name].[hash:8].js',
76 | chunkFilename: 'static/js/[name].[hash:8].js',
77 | publicPath: publicPath,
78 | crossOriginLoading: 'anonymous',
79 | devtoolModuleFilenameTemplate: info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'),
80 | globalObject: 'this'
81 | },
82 | optimization: {
83 | runtimeChunk: 'single',
84 | splitChunks: {
85 | chunks: 'async',
86 | name: false,
87 | cacheGroups: {
88 | vendors: {
89 | chunks: 'all',
90 | test: '_vendor_',
91 | name: 'vendor'
92 | },
93 | i18n: {
94 | chunks: 'all',
95 | test: /utils\/i18n|locals\/\w+\.json/,
96 | enforce: true,
97 | name: 'i18n'
98 | }
99 | }
100 | }
101 | },
102 | resolve: {
103 | modules: ['node_modules', paths.appNodeModules, paths.root].concat(paths.nodePaths),
104 | extensions: paths.moduleFileExtensions,
105 | alias: Object.assign(
106 | {
107 | 'react-native': 'react-native-web'
108 | },
109 | paths.alias
110 | ),
111 | plugins: [
112 | new DirectoryNamedWebpackPlugin({
113 | honorIndex: true,
114 | exclude: /node_modules|libs/
115 | })
116 | ]
117 | },
118 |
119 | module: {
120 | strictExportPresence: true,
121 | rules: [
122 | { parser: { requireEnsure: false } },
123 |
124 | {
125 | test: /\.(js|mjs|jsx|ts|tsx)$/,
126 | enforce: 'pre',
127 | use: [
128 | {
129 | options: {
130 | cache: true,
131 | formatter: require.resolve('react-dev-utils/eslintFormatter'),
132 | eslintPath: require.resolve('eslint'),
133 | resolvePluginsRelativeTo: __dirname
134 | },
135 | loader: require.resolve('eslint-loader')
136 | }
137 | ],
138 | include: [paths.formutilSrc, paths.appSrc]
139 | },
140 | {
141 | oneOf: [
142 | {
143 | test: /\.html$/,
144 | use: [
145 | {
146 | loader: require.resolve('babel-loader'),
147 | options: babelOption
148 | },
149 | {
150 | loader: require.resolve('html-loader'),
151 | options: {
152 | url(url) {
153 | return !/\.(webp|png|jpeg|jpg|gif|svg|mp3|wmv|mp4|ogg|webm|s[ac]ss|css|less|m?[tj]sx?)$/.test(
154 | url
155 | );
156 | },
157 | import: true
158 | }
159 | }
160 | ]
161 | },
162 | {
163 | test: /\.(js|mjs|jsx|ts|tsx)$/,
164 | include: [paths.formutilSrc, paths.appSrc],
165 | loader: require.resolve('babel-loader'),
166 | options: {
167 | customize: require.resolve('babel-preset-react-app/webpack-overrides'),
168 | plugins: [
169 | [
170 | require.resolve('babel-plugin-named-asset-import'),
171 | {
172 | loaderMap: {
173 | svg: {
174 | ReactComponent: '@svgr/webpack?-svgo,+titleProp,+ref![path]'
175 | }
176 | }
177 | }
178 | ]
179 | ],
180 | cacheDirectory: true,
181 | cacheCompression: false,
182 | rootMode: 'upward'
183 | }
184 | },
185 | {
186 | test: /\.(js|mjs)$/,
187 | exclude: /@babel(?:\/|\\{1,2})runtime/,
188 | loader: require.resolve('babel-loader'),
189 | options: babelOption
190 | },
191 | {
192 | test: /\.css$/,
193 | exclude: /\.module\.css$/,
194 | use: getStyleLoaders({
195 | importLoaders: 1
196 | })
197 | },
198 | // Adds support for CSS Modules (https://github.com/css-modules/css-modules)
199 | // using the extension .module.css
200 | {
201 | test: /\.module\.css$/,
202 | use: getStyleLoaders({
203 | importLoaders: 1,
204 | modules: {
205 | getLocalIdent: getCSSModuleLocalIdent
206 | }
207 | })
208 | },
209 | {
210 | test: /\.s[ac]ss$/,
211 | exclude: /\.module\.s[ac]ss$/,
212 | use: getStyleLoaders({ importLoaders: 2 }, 'sass-loader')
213 | },
214 | {
215 | test: /\.module\.s[ac]ss$/,
216 | use: getStyleLoaders(
217 | {
218 | importLoaders: 2,
219 | modules: {
220 | getLocalIdent: getCSSModuleLocalIdent
221 | }
222 | },
223 | 'sass-loader'
224 | )
225 | },
226 | {
227 | test: /\.less$/,
228 | exclude: /\.module\.less$/,
229 | use: getStyleLoaders({ importLoaders: 2 }, 'less-loader')
230 | },
231 | {
232 | test: /\.module\.less$/,
233 | use: getStyleLoaders(
234 | {
235 | importLoaders: 2,
236 | modules: {
237 | getLocalIdent: getCSSModuleLocalIdent
238 | }
239 | },
240 | 'less-loader'
241 | )
242 | },
243 | {
244 | test: /\.(txt|htm)$/,
245 | loader: require.resolve('raw-loader')
246 | },
247 | {
248 | test: /\.(mp4|webm|wav|mp3|m4a|aac|oga)$/,
249 | loader: require.resolve('file-loader'),
250 | options: {
251 | name: 'static/media/[name].[hash:8].[ext]'
252 | }
253 | },
254 | {
255 | exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/, /\.(txt|htm)$/],
256 | loader: require.resolve('file-loader'),
257 | options: {
258 | name: 'static/images/[name].[hash:8].[ext]'
259 | }
260 | }
261 | ]
262 | }
263 | ]
264 | },
265 | plugins: injects
266 | .concat([
267 | isBuilding &&
268 | new MiniCssExtractPlugin({
269 | filename: 'static/css/[name].[hash:8].css',
270 | ignoreOrder: !!pkg.ignoreCssOrderWarnings || process.env.IGNORE_CSS_ORDER_WARNINGS === 'true'
271 | }),
272 | new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
273 | new ModuleNotFoundPlugin(paths.root),
274 | new webpack.EnvironmentPlugin(env.raw),
275 | new CaseSensitivePathsPlugin(),
276 | new webpack.IgnorePlugin({
277 | resourceRegExp: /^\.\/locale$/,
278 | contextRegExp: /moment$/
279 | }),
280 | new ForkTsCheckerWebpackPlugin({
281 | typescript: resolve.sync('typescript', {
282 | basedir: paths.appNodeModules
283 | }),
284 | async: true,
285 | useTypescriptIncrementalApi: true,
286 | checkSyntacticErrors: true,
287 | tsconfig: paths.appTsConfig,
288 | compilerOptions: {
289 | jsx: 'preserve',
290 | checkJs: false
291 | },
292 | reportFiles: ['**/*.(ts|tsx)', '!**/__tests__/**', '!**/?(*.)(spec|test).*'],
293 | silent: true
294 | }),
295 | new webpack.BannerPlugin({
296 | banner: '@author ' + pkg.author,
297 | entryOnly: true
298 | })
299 | ])
300 | .filter(Boolean),
301 |
302 | node: {
303 | dgram: 'empty',
304 | fs: 'empty',
305 | net: 'empty',
306 | tls: 'empty',
307 | child_process: 'empty'
308 | },
309 |
310 | performance: false
311 | };
312 |
313 | function getStyleLoaders(cssOptions, preProcessor) {
314 | const loaders = [
315 | isBuilding
316 | ? {
317 | loader: MiniCssExtractPlugin.loader,
318 | options: { publicPath: '../../', sourceMap: true, esModule: true }
319 | }
320 | : require.resolve('style-loader'),
321 | {
322 | loader: require.resolve('css-loader'),
323 | options: Object.assign({ sourceMap: true }, cssOptions)
324 | },
325 | {
326 | loader: require.resolve('postcss-loader'),
327 | options: {
328 | ident: 'postcss',
329 | plugins: () => [
330 | require('postcss-flexbugs-fixes'),
331 | require('postcss-preset-env')({
332 | autoprefixer: {
333 | flexbox: 'no-2009'
334 | },
335 | stage: 3
336 | })
337 | ],
338 | sourceMap: true
339 | }
340 | }
341 | ];
342 |
343 | if (preProcessor) {
344 | loaders.push({
345 | loader: require.resolve(preProcessor),
346 | options: Object.assign(
347 | {},
348 | { sourceMap: true },
349 | preProcessor === 'less-loader'
350 | ? {
351 | javascriptEnabled: true
352 | }
353 | : {
354 | implementation: require('sass')
355 | }
356 | )
357 | });
358 | }
359 |
360 | return loaders;
361 | }
362 |
--------------------------------------------------------------------------------
/docs/scripts/i18n.js:
--------------------------------------------------------------------------------
1 | /* eslint @typescript-eslint/no-var-requires: 0 */
2 | const fs = require('fs-extra');
3 | const glob = require('glob');
4 | const Parser = require('i18next-scanner').Parser;
5 | const xlsx = require('node-xlsx');
6 | const paths = require('./config/paths');
7 | const path = require('path');
8 | const chalk = require('chalk');
9 | const ora = require('ora');
10 | const lodash = require('lodash');
11 | const pkg = require(paths.appPackageJson);
12 |
13 | const spinner = ora();
14 |
15 | const xlsxOptions = {
16 | '!cols': [{ wch: 50 }, { wch: 50 }]
17 | };
18 |
19 | const terminalArg = process.argv[2];
20 |
21 | if (terminalArg === '--scan') {
22 | ensureLocalsConfig();
23 | scanner();
24 | } else if (terminalArg === '--read') {
25 | ensureLocalsConfig();
26 | reader();
27 | }
28 |
29 | function ensureLocalsConfig() {
30 | if (Array.isArray(pkg.locals) === false) {
31 | spinner.fail(chalk.red('未在 package.json 中找到相关语言包配置!'));
32 |
33 | spinner.warn(
34 | chalk.yellow('需要 package.json 中添加 { "locals": ["zh_CN", "en_US"] } 配置后,才能运行该命令!')
35 | );
36 |
37 | process.exit(0);
38 | }
39 | }
40 |
41 | /**
42 | * @description
43 | * 扫描源代码文件,匹配需要翻译的文案,并输出excel文件待翻译
44 | */
45 | function scanner() {
46 | const i18nParser = new Parser({
47 | lngs: pkg.locals,
48 | nsSeparator: false,
49 | keySeparator: false,
50 | pluralSeparator: false,
51 | contextSeparator: false
52 | });
53 |
54 | fs.ensureDirSync(path.join(paths.locals, 'xlsx'));
55 |
56 | glob.sync(paths.appSrc + '/**/*.{js,jsx,ts,tsx}').forEach(file => {
57 | const content = fs.readFileSync(file);
58 |
59 | i18nParser.parseFuncFromString(content, { list: ['__', 'i18n.__', 'window.__'] }, key => {
60 | if (key) {
61 | i18nParser.set(key, key);
62 | }
63 | });
64 | });
65 |
66 | const i18nJson = i18nParser.get();
67 |
68 | Object.keys(i18nJson).forEach(key => {
69 | const jsonDestination = path.join(paths.locals, key + '.json');
70 | const excelDestination = path.join(paths.locals, 'xlsx', key + '.xlsx');
71 |
72 | const translation = i18nJson[key].translation;
73 | const existConfig = fs.existsSync(jsonDestination) ? JSON.parse(fs.readFileSync(jsonDestination)) : {};
74 | const newConfig = lodash.pickBy(existConfig, (value, key) => key in translation);
75 |
76 | lodash.each(translation, (value, key) => {
77 | if (!(key in newConfig)) {
78 | newConfig[key] = value;
79 | }
80 | });
81 |
82 | fs.outputFile(path.join(paths.locals, key + '.json'), JSON.stringify(newConfig, '\n', 2));
83 |
84 | convertJson2Excel(newConfig, key, path.join(excelDestination));
85 |
86 | spinner.succeed('输出 ' + chalk.bold(chalk.green(key)) + ' 到 ' + chalk.cyan(excelDestination));
87 | });
88 |
89 | console.log();
90 | spinner.warn(chalk.yellow('你可以将生成的excel文件进行翻译后,放回原处。然后运行:'));
91 | console.log(chalk.green(' npm run i18n-read'));
92 | }
93 |
94 | /**
95 | * @description
96 | * 读取excel文件,并转换为json语言包
97 | */
98 | function reader() {
99 | glob.sync(path.join(paths.locals, 'xlsx', '!(~$)*.xlsx')).forEach(file => {
100 | const lang = path.basename(file, '.xlsx');
101 | const jsonDestination = path.join(paths.locals, lang + '.json');
102 |
103 | convertExcel2Json(file, lang, jsonDestination);
104 |
105 | spinner.succeed('输出 ' + chalk.bold(chalk.green(lang)) + ' 到 ' + chalk.cyan(jsonDestination));
106 | });
107 |
108 | console.log();
109 | spinner.succeed(chalk.green('语言包转换成功!'));
110 | }
111 |
112 | function convertJson2Excel(jsonContent, lang, destination) {
113 | const sheets = [[pkg.name + ' v' + pkg.version, lang], ['原始文案(禁止修改)', '翻译文案'], []];
114 |
115 | Object.keys(jsonContent).forEach(key => {
116 | const text = jsonContent[key];
117 |
118 | sheets.push([key, text]);
119 | });
120 |
121 | const buffer = xlsx.build([{ name: 'locals', data: sheets }], xlsxOptions);
122 |
123 | fs.writeFileSync(destination, buffer);
124 | }
125 |
126 | function convertExcel2Json(file, lang, destination) {
127 | const sheets = xlsx.parse(fs.readFileSync(file));
128 |
129 | const jsonData = require(destination) || {};
130 |
131 | sheets[0].data.slice(2).forEach(item => {
132 | if (item.length) {
133 | jsonData[item[0]] = item[1];
134 | }
135 | });
136 |
137 | fs.outputFileSync(destination, JSON.stringify(jsonData, '\n', 2));
138 | }
139 |
140 | exports.ensureLocals = function() {
141 | fs.ensureDirSync(path.join(paths.locals));
142 |
143 | if (Array.isArray(pkg.locals)) {
144 | pkg.locals.forEach(lang => {
145 | const file = path.join(paths.locals, lang + '.json');
146 |
147 | if (!fs.existsSync(file)) {
148 | fs.outputJSONSync(file, {});
149 | }
150 | });
151 | }
152 | };
153 |
--------------------------------------------------------------------------------
/docs/scripts/start.js:
--------------------------------------------------------------------------------
1 | /* eslint @typescript-eslint/no-var-requires: 0 */
2 | process.env.BABEL_ENV = 'development';
3 | process.env.NODE_ENV = 'development';
4 |
5 | process.on('unhandledRejection', err => {
6 | throw err;
7 | });
8 |
9 | require('./config/env');
10 |
11 | const chalk = require('chalk');
12 | const webpack = require('webpack');
13 | const ora = require('ora');
14 | const WebpackDevServer = require('webpack-dev-server');
15 | const { choosePort, prepareProxy, createCompiler, createDevServerConfig } = require('./config/helper');
16 | const { prepareUrls } = require('react-dev-utils/WebpackDevServerUtils');
17 | const openBrowser = require('react-dev-utils/openBrowser');
18 | const clearConsole = require('react-dev-utils/clearConsole');
19 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
20 | const checkMissDependencies = require('./config/checkMissDependencies');
21 | const config = require('./config/webpack.config.dev');
22 | const paths = require('./config/paths');
23 | const { ensureLocals } = require('./i18n');
24 | const pkg = require(paths.appPackageJson);
25 |
26 | const spinner = ora('webpack启动中...').start();
27 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
28 | const HOST = process.env.HOST || '0.0.0.0';
29 | const isInteractive = process.stdout.isTTY;
30 |
31 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
32 | console.log();
33 | process.exit(1);
34 | }
35 |
36 | ensureLocals();
37 |
38 | checkMissDependencies(spinner)
39 | .then(() => {
40 | return choosePort(HOST, DEFAULT_PORT, spinner).then(port => {
41 | if (port === null) {
42 | console.log();
43 |
44 | spinner.fail(
45 | '请关闭占用 ' +
46 | chalk.bold(chalk.yellow(DEFAULT_PORT)) +
47 | ' 端口的程序后再运行;或者指定一个新的端口:' +
48 | chalk.bold(chalk.yellow('PORT=4000 npm start'))
49 | );
50 |
51 | console.log();
52 | process.exit(0);
53 | } else {
54 | spinner.start();
55 |
56 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
57 | const appName = pkg.name;
58 | const urls = prepareUrls(protocol, HOST, port);
59 | const devSocket = {
60 | warnings: warnings => devServer.sockWrite(devServer.sockets, 'warnings', warnings),
61 | errors: errors => devServer.sockWrite(devServer.sockets, 'errors', errors)
62 | };
63 | const compiler = createCompiler(webpack, config, appName, urls, devSocket, spinner);
64 | const proxyConfig = prepareProxy(process.env.PROXY || pkg.proxy, paths.appPublic);
65 | const serverConfig = createDevServerConfig(proxyConfig, urls.lanUrlForConfig);
66 | const devServer = new WebpackDevServer(compiler, serverConfig);
67 |
68 | // Launch WebpackDevServer.
69 | devServer.listen(port, HOST, err => {
70 | if (err) {
71 | return console.log(err);
72 | }
73 |
74 | if (isInteractive) {
75 | clearConsole();
76 | }
77 |
78 | spinner.text = chalk.cyan('正在启动测试服务器...');
79 | openBrowser(urls.localUrlForBrowser);
80 | });
81 |
82 | ['SIGINT', 'SIGTERM'].forEach(function(sig) {
83 | process.on(sig, function() {
84 | devServer.close();
85 | spinner.stop();
86 | process.exit();
87 | });
88 | });
89 | }
90 | });
91 | })
92 | .catch(function(err) {
93 | if (err) {
94 | console.log(err.message || err);
95 | console.log();
96 | }
97 |
98 | process.kill(process.pid, 'SIGINT');
99 | });
100 |
--------------------------------------------------------------------------------
/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES5",
4 | "allowJs": true,
5 | "checkJs": true,
6 | "module": "ESNext",
7 | "lib": ["ESNext", "DOM"],
8 | "jsx": "react",
9 | "resolveJsonModule": true,
10 | "experimentalDecorators": true,
11 | "allowSyntheticDefaultImports": true,
12 | "allowUnreachableCode": false,
13 | "moduleResolution": "node",
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "skipLibCheck": true,
17 | "noEmit": true,
18 | "noImplicitReturns": true,
19 | "noImplicitThis": true,
20 | "noImplicitAny": false,
21 | "importHelpers": true,
22 | "strictNullChecks": true,
23 | "suppressImplicitAnyIndexErrors": true,
24 | "noUnusedLocals": false,
25 | "baseUrl": ".",
26 | "paths": {
27 | "*": ["*", "app/*"]
28 | }
29 | },
30 | "exclude": ["scripts", "node_modules", "dist", "demo", "buildDev"]
31 | }
32 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | const pkg = require('./package.json');
2 |
3 | /**
4 | * 0: off
5 | * 1: warn
6 | * 2: error
7 | */
8 | module.exports = {
9 | overrides: [
10 | {
11 | files: ['**/__tests__/**/*', '**/*.{spec,test}.*'],
12 | rules: {
13 | 'jest/consistent-test-it': [1, { fn: 'test' }],
14 | 'jest/expect-expect': 1,
15 | 'jest/no-deprecated-functions': 2
16 | }
17 | }
18 | ],
19 | settings: {
20 | 'import/core-modules': [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})]
21 | },
22 | rules: {
23 | 'react/react-in-jsx-scope': 2,
24 | 'react/no-unsafe': [2, { checkAliases: true }],
25 | 'react/no-deprecated': 2,
26 | 'import/no-anonymous-default-export': [
27 | 2,
28 | {
29 | allowArray: true,
30 | allowArrowFunction: false,
31 | allowAnonymousClass: false,
32 | allowAnonymousFunction: false,
33 | allowCallExpression: true, // The true value here is for backward compatibility
34 | allowLiteral: true,
35 | allowObject: true
36 | }
37 | ],
38 | 'import/no-duplicates': 1,
39 | 'import/order': [
40 | 1,
41 | {
42 | groups: ['builtin', 'external', 'internal', ['parent', 'sibling', 'index'], 'object', 'unknown']
43 | }
44 | ],
45 | 'import/no-useless-path-segments': [
46 | 1,
47 | {
48 | noUselessIndex: true
49 | }
50 | ],
51 | 'lines-between-class-members': [1, 'always', { exceptAfterSingleLine: true }],
52 | 'padding-line-between-statements': [
53 | 1,
54 | {
55 | blankLine: 'always',
56 | prev: [
57 | 'multiline-block-like',
58 | 'multiline-expression',
59 | 'const',
60 | 'let',
61 | 'var',
62 | 'cjs-import',
63 | 'import',
64 | 'export',
65 | 'cjs-export',
66 | 'class',
67 | 'throw',
68 | 'directive'
69 | ],
70 | next: '*'
71 | },
72 | {
73 | blankLine: 'always',
74 | prev: '*',
75 | next: [
76 | 'multiline-block-like',
77 | 'multiline-expression',
78 | 'const',
79 | 'let',
80 | 'var',
81 | 'cjs-import',
82 | 'import',
83 | 'export',
84 | 'cjs-export',
85 | 'class',
86 | 'throw',
87 | 'return'
88 | ]
89 | },
90 | { blankLine: 'any', prev: ['cjs-import', 'import'], next: ['cjs-import', 'import'] },
91 | { blankLine: 'any', prev: ['export', 'cjs-export'], next: ['export', 'cjs-export'] },
92 | { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] }
93 | ]
94 | }
95 | };
96 |
--------------------------------------------------------------------------------
/hooks.d.ts:
--------------------------------------------------------------------------------
1 | // Type definitions for react-formutil/hooks@>0.5.0
2 | // Project: react-formutil
3 | // Definitions by: qiqiboy
4 |
5 | export * from './index';
6 |
--------------------------------------------------------------------------------
/hooks.js:
--------------------------------------------------------------------------------
1 | module.exports = require('.');
2 |
--------------------------------------------------------------------------------
/jest/cssTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // This is a custom Jest transformer turning style imports into empty objects.
4 | // http://facebook.github.io/jest/docs/en/webpack.html
5 |
6 | module.exports = {
7 | process() {
8 | return 'module.exports = {};';
9 | },
10 | getCacheKey() {
11 | // The output is always the same.
12 | return 'cssTransform';
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/jest/fileTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const camelcase = require('camelcase');
5 |
6 | // This is a custom Jest transformer turning file imports into filenames.
7 | // http://facebook.github.io/jest/docs/en/webpack.html
8 |
9 | module.exports = {
10 | process(src, filename) {
11 | const assetFilename = JSON.stringify(path.basename(filename));
12 |
13 | if (filename.match(/\.svg$/)) {
14 | // Based on how SVGR generates a component name:
15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
16 | const pascalCaseFilename = camelcase(path.parse(filename).name, {
17 | pascalCase: true,
18 | });
19 | const componentName = `Svg${pascalCaseFilename}`;
20 | return `const React = require('react');
21 | module.exports = {
22 | __esModule: true,
23 | default: ${assetFilename},
24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
25 | return {
26 | $$typeof: Symbol.for('react.element'),
27 | type: 'svg',
28 | ref: ref,
29 | key: null,
30 | props: Object.assign({}, props, {
31 | children: ${assetFilename}
32 | })
33 | };
34 | }),
35 | };`;
36 | }
37 |
38 | return `module.exports = ${assetFilename};`;
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/jest/jest.config.js:
--------------------------------------------------------------------------------
1 | // For a detailed explanation regarding each configuration property, visit:
2 | // https://jestjs.io/docs/en/configuration.html
3 | const fs = require('fs-extra');
4 |
5 | module.exports = {
6 | projects: [
7 | {
8 | displayName: 'lint',
9 | runner: 'eslint',
10 | rootDir: process.cwd(),
11 | roots: ['/src', fs.existsSync(process.cwd() + '/tests') && '/tests'].filter(Boolean),
12 | testMatch: [
13 | '/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
14 | '/{src,tests}/**/*.{spec,test}.{js,jsx,ts,tsx}'
15 | ]
16 | },
17 | {
18 | displayName: 'test',
19 | rootDir: process.cwd(),
20 | roots: ['/src', fs.existsSync(process.cwd() + '/tests') && '/tests'].filter(Boolean),
21 | collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'],
22 | setupFiles: [],
23 | setupFilesAfterEnv: ['/jest/setupTests.ts'],
24 | testMatch: [
25 | '/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
26 | '/{src,tests}/**/*.{spec,test}.{js,jsx,ts,tsx}'
27 | ],
28 | testEnvironment: 'jest-environment-jsdom-fourteen',
29 | transform: {
30 | '^.+\\.(js|jsx|ts|tsx)$': '/node_modules/babel-jest',
31 | '^.+\\.(css|less|sass|scss$)': '/jest/cssTransform.js',
32 | '^(?!.*\\.(js|jsx|ts|tsx|css|less|sass|scss|json)$)': '/jest/fileTransform.js'
33 | },
34 | transformIgnorePatterns: [
35 | '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$',
36 | '^.+\\.module\\.(css|sass|scss|less)$'
37 | ],
38 | modulePaths: [],
39 | moduleNameMapper: {
40 | '^react-native$': 'react-native-web',
41 | '^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy'
42 | },
43 | moduleFileExtensions: ['web.js', 'js', 'web.ts', 'ts', 'web.tsx', 'tsx', 'json', 'web.jsx', 'jsx', 'node'],
44 | verbose: true,
45 | // resetMocks: true,
46 | watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname']
47 | }
48 | ]
49 | };
50 |
--------------------------------------------------------------------------------
/jest/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/jest/test.js:
--------------------------------------------------------------------------------
1 | // Do this as the first thing so that any code reading it knows the right env.
2 | process.env.BABEL_ENV = 'test';
3 | process.env.NODE_ENV = 'test';
4 | process.env.PUBLIC_URL = '';
5 |
6 | // Makes the script crash on unhandled rejections instead of silently
7 | // ignoring them. In the future, promise rejections that are not handled will
8 | // terminate the Node.js process with a non-zero exit code.
9 | process.on('unhandledRejection', err => {
10 | throw err;
11 | });
12 |
13 | const jest = require('jest');
14 | const execSync = require('child_process').execSync;
15 | let argv = process.argv.slice(2);
16 |
17 | argv.push('-c', 'jest/jest.config.js');
18 |
19 | function isInGitRepository() {
20 | try {
21 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
22 |
23 | return true;
24 | } catch (e) {
25 | return false;
26 | }
27 | }
28 |
29 | function isInMercurialRepository() {
30 | try {
31 | execSync('hg --cwd . root', { stdio: 'ignore' });
32 |
33 | return true;
34 | } catch (e) {
35 | return false;
36 | }
37 | }
38 |
39 | // Watch unless on CI or explicitly running all tests
40 | if (!process.env.CI && argv.indexOf('--watchAll') === -1 && argv.indexOf('--watchAll=false') === -1) {
41 | // https://github.com/facebook/create-react-app/issues/5210
42 | const hasSourceControl = isInGitRepository() || isInMercurialRepository();
43 |
44 | argv.push(hasSourceControl ? '--watch' : '--watchAll');
45 | }
46 |
47 | jest.run(argv);
48 |
--------------------------------------------------------------------------------
/npm/index.cjs.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 | module.exports = require('./react-formutil.cjs.production.js');
3 | } else {
4 | module.exports = require('./react-formutil.cjs.development.js');
5 | }
6 |
--------------------------------------------------------------------------------
/npm/index.esm.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 | module.exports = require('./react-formutil.esm.production.js');
3 | } else {
4 | module.exports = require('./react-formutil.esm.development.js');
5 | }
6 |
--------------------------------------------------------------------------------
/npm/index.umd.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 | module.exports = require('./react-formutil.umd.production.js');
3 | } else {
4 | module.exports = require('./react-formutil.umd.development.js');
5 | }
6 |
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-formutil",
3 | "version": "1.1.9",
4 | "description": "Happy to build the forms in React ^_^",
5 | "main": "dist/index.cjs.js",
6 | "module": "dist/index.esm.js",
7 | "directories": {
8 | "doc": "docs"
9 | },
10 | "entryFile": "src/index.js",
11 | "scripts": {
12 | "start": "cd docs && npm start",
13 | "build": "npm run clear && rollup -c",
14 | "clear": "rimraf dist",
15 | "test": "node jest/test.js",
16 | "tsc": "node -e \"require('fs-extra').outputJsonSync('.git-tsconfig.json',{ extends: './tsconfig.json', include: ['*.d.ts'].concat(process.env.StagedFiles.split(/\\n+/)) })\" && echo 'TS checking...\\n' && tsc -p .git-tsconfig.json --noEmit --checkJs false"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "git+https://github.com/qiqiboy/react-formutil.git"
21 | },
22 | "keywords": [
23 | "react",
24 | "react-components",
25 | "react-form",
26 | "react-formutil",
27 | "forms",
28 | "create-form",
29 | "umd"
30 | ],
31 | "author": "qiqiboy",
32 | "license": "ISC",
33 | "bugs": {
34 | "url": "https://github.com/qiqiboy/react-formutil/issues"
35 | },
36 | "eslintConfig": {
37 | "extends": [
38 | "react-app",
39 | "react-app/jest",
40 | "./eslint.config.js"
41 | ]
42 | },
43 | "prettier": {
44 | "printWidth": 120,
45 | "tabWidth": 4,
46 | "trailingComma": "none",
47 | "jsxBracketSameLine": true,
48 | "semi": true,
49 | "arrowParens": "avoid",
50 | "singleQuote": true,
51 | "overrides": [
52 | {
53 | "files": "*.json",
54 | "options": {
55 | "tabWidth": 2
56 | }
57 | }
58 | ]
59 | },
60 | "homepage": "https://github.com/qiqiboy/react-formutil#readme",
61 | "peerDependencies": {
62 | "react": "^16.3.0 || ^17.0.0 || ^18.0.0",
63 | "prop-types": "^15.0.0"
64 | },
65 | "types": "./index.d.ts",
66 | "devDependencies": {
67 | "@babel/cli": "7.12.1",
68 | "@babel/core": "7.12.3",
69 | "@commitlint/cli": "11.0.0",
70 | "@commitlint/config-conventional": "11.0.0",
71 | "@rollup/plugin-babel": "5.2.1",
72 | "@rollup/plugin-commonjs": "11.1.0",
73 | "@rollup/plugin-eslint": "8.0.0",
74 | "@rollup/plugin-node-resolve": "10.0.0",
75 | "@rollup/plugin-replace": "2.3.4",
76 | "@testing-library/jest-dom": "5.11.5",
77 | "@testing-library/react": "11.1.2",
78 | "@testing-library/user-event": "12.2.2",
79 | "@types/jest": "26.0.15",
80 | "@types/node": "14.14.7",
81 | "@types/react": "16.9.23",
82 | "@types/react-dom": "16.9.5",
83 | "@types/react-is": "16.7.1",
84 | "@typescript-eslint/eslint-plugin": "4.7.0",
85 | "@typescript-eslint/parser": "4.7.0",
86 | "babel-eslint": "10.1.0",
87 | "babel-jest": "26.6.3",
88 | "babel-preset-react-app": "10.0.0",
89 | "eslint": "7.13.0",
90 | "eslint-config-react-app": "6.0.0",
91 | "eslint-plugin-flowtype": "5.2.0",
92 | "eslint-plugin-import": "2.22.1",
93 | "eslint-plugin-jest": "24.1.3",
94 | "eslint-plugin-jsx-a11y": "6.4.1",
95 | "eslint-plugin-react": "7.21.5",
96 | "eslint-plugin-react-hooks": "4.2.0",
97 | "eslint-plugin-testing-library": "3.10.0",
98 | "husky": "3.1.0",
99 | "identity-obj-proxy": "3.0.0",
100 | "jest": "26.6.3",
101 | "jest-environment-jsdom-fourteen": "1.0.1",
102 | "jest-resolve": "26.6.2",
103 | "jest-runner-eslint": "0.10.0",
104 | "jest-watch-typeahead": "0.4.2",
105 | "lint-staged": "10.5.1",
106 | "prettier": "2.1.2",
107 | "prop-types": "15.7.2",
108 | "react": "17.0.1",
109 | "react-app-polyfill": "^1.0.6",
110 | "react-dom": "17.0.1",
111 | "react-hot-loader": "^4.12.19",
112 | "rimraf": "3.0.2",
113 | "rollup": "2.33.1",
114 | "rollup-plugin-clear": "^2.0.7",
115 | "rollup-plugin-copy": "3.3.0",
116 | "rollup-plugin-filesize": "9.0.2",
117 | "rollup-plugin-sass": "1.2.2",
118 | "rollup-plugin-terser": "7.0.2",
119 | "typescript": "5.2.2"
120 | },
121 | "dependencies": {
122 | "hoist-non-react-statics": "^3.3.2",
123 | "react-fast-compare": "^3.0.1",
124 | "warning": "^4.0.3"
125 | },
126 | "sideEffects": false,
127 | "husky": {
128 | "hooks": {
129 | "commit-msg": "node_modules/.bin/commitlint --edit $HUSKY_GIT_PARAMS",
130 | "pre-commit": "lint-staged && export StagedFiles=$(git diff --diff-filter AM --name-only --relative --staged | grep -E '^src/.*\\.m?[jt]sx?$') && if [ -n \"$StagedFiles\" ]; then npm run tsc ; fi"
131 | }
132 | },
133 | "lint-staged": {
134 | "src/**/*.{js,jsx,mjs,ts,tsx}": [
135 | "node_modules/.bin/prettier --write",
136 | "node_modules/.bin/eslint --fix"
137 | ],
138 | "src/**/*.{css,scss,less,json,html,md}": [
139 | "node_modules/.bin/prettier --write"
140 | ]
141 | },
142 | "stylelint": {
143 | "extends": "stylelint-config-recommended"
144 | },
145 | "browserslist": [
146 | ">0.2%",
147 | "not dead",
148 | "not op_mini all"
149 | ],
150 | "config": {
151 | "commitizen": {
152 | "path": "cz-conventional-changelog"
153 | }
154 | },
155 | "engines": {
156 | "node": ">=10.13.0",
157 | "tiger-new": "6.2.7"
158 | },
159 | "commitlint": {
160 | "extends": [
161 | "@commitlint/config-conventional"
162 | ],
163 | "rules": {
164 | "subject-case": [
165 | 0
166 | ],
167 | "scope-case": [
168 | 0
169 | ]
170 | }
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = 'production';
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 | const commonjs = require('@rollup/plugin-commonjs');
6 | const replace = require('@rollup/plugin-replace');
7 | const { nodeResolve } = require('@rollup/plugin-node-resolve');
8 | const babel = require('@rollup/plugin-babel').default;
9 | const filesize = require('rollup-plugin-filesize');
10 | const copy = require('rollup-plugin-copy');
11 | const sass = require('rollup-plugin-sass');
12 | const { terser } = require('rollup-plugin-terser');
13 | const eslint = require('@rollup/plugin-eslint');
14 | const pkg = require('./package.json');
15 |
16 | /**
17 | * 如果希望将某些模块代码直接构建进输出文件,可以再这里指定这些模块名称
18 | */
19 | const externalExclude = [
20 | '@babel/runtime', 'regenerator-runtime'
21 | ];
22 |
23 | const exportName = pkg.exportName || pkg.name.split('/').slice(-1)[0];
24 | /**
25 | * 如果你希望编译后的代码里依然自动包含进去编译后的css,那么这里可以设置为 true
26 | */
27 | const shouldPreserveCss = false;
28 |
29 | function createConfig(env, module) {
30 | const isProd = env === 'production';
31 | // for umd globals
32 | const globals = {
33 | react: 'React',
34 | 'react-dom': 'ReactDOM',
35 | 'prop-types': 'PropTypes'
36 | };
37 |
38 | return {
39 | /**
40 | * 入口文件位置,如果你更改了entryFile,别忘了同时修改 npm/index.cjs.js 和 npm/index.esm.js 里的文件引用名称
41 | */
42 | input: pkg.entryFile || 'src/index.ts',
43 | external:
44 | module === 'umd'
45 | ? Object.keys(globals)
46 | : id =>
47 | !externalExclude.some(name => id.startsWith(name)) && !id.startsWith('.') && !path.isAbsolute(id),
48 | output: {
49 | name: 'ReactFormutil',
50 | file: `dist/${exportName}.${module}.${env}.js`,
51 | format: module,
52 | exports: 'named',
53 | sourcemap: false,
54 | intro:
55 | module !== 'umd' && shouldPreserveCss
56 | ? module === 'cjs'
57 | ? `require('./${exportName}.css');`
58 | : `import('./${exportName}.css');`
59 | : undefined,
60 | globals
61 | },
62 | treeshake: {
63 | /**
64 | * 如果你有引入一些有副作用的代码模块,或者构建后的代码运行异常,可以尝试将该项设置为 true
65 | */
66 | moduleSideEffects: false
67 | },
68 | plugins: [
69 | eslint({
70 | fix: true,
71 | throwOnError: true,
72 | throwOnWarning: true
73 | }),
74 | nodeResolve({
75 | extensions: ['.js', '.jsx', '.ts', '.tsx']
76 | }),
77 | commonjs({
78 | include: /node_modules/
79 | }),
80 | replace({
81 | 'process.env.NODE_ENV': JSON.stringify(env)
82 | }),
83 | babel({
84 | exclude: 'node_modules/**',
85 | extensions: ['.js', '.jsx', '.ts', '.tsx'],
86 | babelHelpers: 'runtime',
87 | babelrc: false,
88 | configFile: false,
89 | presets: [
90 | [
91 | '@babel/preset-env',
92 | {
93 | useBuiltIns: 'entry',
94 | corejs: 3,
95 | modules: false,
96 | exclude: ['transform-typeof-symbol']
97 | }
98 | ],
99 | [
100 | '@babel/preset-react',
101 | {
102 | development: false,
103 | useBuiltIns: true,
104 | runtime: 'classic'
105 | }
106 | ],
107 | ['@babel/preset-typescript']
108 | ],
109 | plugins: [
110 | 'babel-plugin-macros',
111 | ['@babel/plugin-proposal-decorators', { legacy: true }],
112 | [
113 | '@babel/plugin-proposal-class-properties',
114 | {
115 | loose: true
116 | }
117 | ],
118 | [
119 | '@babel/plugin-transform-runtime',
120 | {
121 | version: require('@babel/helpers/package.json').version,
122 | corejs: false,
123 | helpers: true,
124 | regenerator: true,
125 | useESModules: module === 'esm',
126 | absoluteRuntime: false
127 | }
128 | ],
129 | isProd && [
130 | // Remove PropTypes from production build
131 | 'babel-plugin-transform-react-remove-prop-types',
132 | {
133 | removeImport: true
134 | }
135 | ],
136 | require('@babel/plugin-proposal-optional-chaining').default,
137 | require('@babel/plugin-proposal-nullish-coalescing-operator').default,
138 | // Adds Numeric Separators
139 | require('@babel/plugin-proposal-numeric-separator').default
140 | ].filter(Boolean)
141 | }),
142 | module !== 'umd' &&
143 | sass({
144 | output: `dist/${exportName}.css`
145 | }),
146 | isProd &&
147 | terser({
148 | output: { comments: false },
149 | compress: false,
150 | warnings: false,
151 | ecma: 5,
152 | ie8: false,
153 | toplevel: true
154 | }),
155 | filesize(),
156 | copy({
157 | targets: [
158 | {
159 | src: `npm/index.${module}.js`,
160 | dest: 'dist'
161 | }
162 | ],
163 | verbose: false
164 | })
165 | ].filter(Boolean)
166 | };
167 | }
168 |
169 | module.exports = ['cjs', 'esm', 'umd'].reduce((configQueue, module) => {
170 | return fs.existsSync(`./npm/index.${module}.js`)
171 | ? configQueue.concat(createConfig('development', module), createConfig('production', module))
172 | : configQueue;
173 | }, []);
174 |
--------------------------------------------------------------------------------
/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/src/EasyField/Group.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Children, cloneElement, createContext } from 'react';
2 | import PropTypes from 'prop-types';
3 | import warning from 'warning';
4 | import { isFunction } from '../utils';
5 |
6 | /** @type {any} */
7 | const { Provider, Consumer } = createContext(() => ({}));
8 |
9 | class EasyFieldGroup extends Component {
10 | static displayName = 'React.Formutil.EasyField.Group';
11 |
12 | static propTypes = {
13 | onChange: PropTypes.func,
14 | onFocus: PropTypes.func,
15 | onBlur: PropTypes.func,
16 |
17 | value: PropTypes.any,
18 | name: PropTypes.string,
19 | type: PropTypes.string.isRequired,
20 | groupNode: PropTypes.any,
21 | children: PropTypes.oneOfType([PropTypes.func, PropTypes.element]).isRequired
22 | };
23 |
24 | static defaultProps = {
25 | type: 'checkbox',
26 | groupNode: 'div'
27 | };
28 |
29 | getGroupContext = () => this.props;
30 |
31 | _render() {
32 | const { className, groupNode: Element, children } = this.props;
33 |
34 | const GroupOptionProps = {
35 | GroupOption: EasyFieldGroupOption,
36 | Field: DeprecatedEasyFieldGroupOption
37 | };
38 |
39 | const childNodes = isFunction(children)
40 | ? children(GroupOptionProps)
41 | : Children.map(children, child => cloneElement(child, GroupOptionProps));
42 |
43 | if (Element === null) {
44 | return childNodes;
45 | }
46 |
47 | return {childNodes};
48 | }
49 |
50 | render() {
51 | return {this._render()};
52 | }
53 | }
54 |
55 | class EasyFieldGroupOption extends Component {
56 | static displayName = 'React.Formutil.EasyField.Group.Option';
57 |
58 | static propTypes = {
59 | $value: PropTypes.any.isRequired
60 | };
61 |
62 | componentDidMount() {
63 | warning('$value' in this.props, `You should pass a $value to .`);
64 | }
65 |
66 | render() {
67 | const { $value, onChange, onFocus, onBlur, ...others } = this.props;
68 |
69 | return (
70 |
71 | {getGroupContext => {
72 | const $groupHandler = getGroupContext();
73 | const { type, name } = $groupHandler;
74 |
75 | const elemProps =
76 | type === 'radio'
77 | ? {
78 | checked: $groupHandler.value === $value,
79 | onChange: ev => {
80 | $groupHandler.onChange($value, ev);
81 |
82 | onChange && onChange(ev);
83 | }
84 | }
85 | : type === 'checkbox'
86 | ? {
87 | checked: $groupHandler.value.indexOf($value) > -1,
88 | onChange: ev => {
89 | $groupHandler.onChange(
90 | ev.target.checked
91 | ? $groupHandler.value.concat($value)
92 | : $groupHandler.value.filter(value => value !== $value),
93 | ev
94 | );
95 |
96 | onChange && onChange(ev);
97 | }
98 | }
99 | : {
100 | value: $groupHandler.value,
101 | onChange: ev => {
102 | $groupHandler.onChange(ev);
103 |
104 | onChange && onChange(ev);
105 | }
106 | };
107 |
108 | return (
109 | {
115 | $groupHandler.onFocus(ev);
116 | onFocus && onFocus(ev);
117 | }}
118 | onBlur={ev => {
119 | $groupHandler.onBlur(ev);
120 | onBlur && onBlur(ev);
121 | }}
122 | />
123 | );
124 | }}
125 |
126 | );
127 | }
128 | }
129 |
130 | class DeprecatedEasyFieldGroupOption extends Component {
131 | static displayName = 'React.Formutil.EasyField.Group.Option.Deprecated';
132 |
133 | componentDidMount() {
134 | warning(
135 | false,
136 | `The "Field" property in EasyField's children-props has been deprecated. Please use "GroupOption" instead.`
137 | );
138 | }
139 |
140 | render() {
141 | return ;
142 | }
143 | }
144 |
145 | export default EasyFieldGroup;
146 |
--------------------------------------------------------------------------------
/src/EasyField/List.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import isEqual from 'react-fast-compare';
4 | import Form from '../Form';
5 | import Field from '../Field';
6 | import { isUndefined, isFunction, runCallback } from '../utils';
7 |
8 | const Wrapper = React.Frament || 'div';
9 |
10 | class EasyFieldList extends Component {
11 | static displayName = 'React.Formutil.EasyField.List';
12 |
13 | static propTypes = {
14 | onChange: PropTypes.func,
15 | onFocus: PropTypes.func,
16 | onBlur: PropTypes.func,
17 | value: PropTypes.array,
18 |
19 | children: PropTypes.func.isRequired
20 | };
21 |
22 | id = 0;
23 | latestValue = this.props.value;
24 | $formutil;
25 |
26 | constructor(props) {
27 | super(props);
28 |
29 | this.state = {
30 | items: props.value.length ? props.value.map(() => this.getId()) : [this.getId()],
31 | formKey: 0
32 | };
33 | }
34 |
35 | componentDidUpdate(prevProps) {
36 | if (this.props.value !== this.latestValue) {
37 | this.setState({
38 | items: this.props.value.length ? this.props.value.map(() => this.getId()) : [this.getId()],
39 | formKey: this.state.formKey + 1
40 | });
41 |
42 | this.latestValue = this.props.value;
43 | }
44 | }
45 |
46 | getId(values) {
47 | return {
48 | id: this.id++,
49 | values
50 | };
51 | }
52 |
53 | FieldValidators = {
54 | required(value) {
55 | return value !== null;
56 | }
57 | };
58 |
59 | $onFormChange = $formutil => {
60 | $formutil.$onValidates($formutil => {
61 | const { $invalid, $params } = $formutil;
62 |
63 | if ($invalid) {
64 | if (this.props.value.length) {
65 | this.props.onChange((this.latestValue = []));
66 | }
67 | } else if (!isEqual(this.props.value, $params.list)) {
68 | this.props.onChange((this.latestValue = $params.list));
69 | }
70 | });
71 | };
72 |
73 | swap = (m, n, callback) =>
74 | this.$setState(({ items }) => {
75 | [items[n], items[m]] = [items[m], items[n]];
76 |
77 | return items;
78 | }, callback);
79 |
80 | insert = (...args) => {
81 | let m, values, callback;
82 |
83 | args.forEach(arg => {
84 | if (isFunction(arg)) {
85 | callback = arg;
86 | } else if (typeof arg === 'number') {
87 | m = arg;
88 | } else if (typeof arg === 'object') {
89 | values = arg;
90 | }
91 | });
92 |
93 | return this.$setState(({ items }) => {
94 | if (isUndefined(m)) {
95 | items.push(this.getId(values));
96 | } else {
97 | items.splice(m, 0, this.getId(values));
98 | }
99 |
100 | return { items };
101 | }, callback);
102 | };
103 |
104 | remove = (...args) => {
105 | let m, callback;
106 |
107 | args.forEach(arg => {
108 | if (isFunction(arg)) {
109 | callback = arg;
110 | } else if (typeof arg === 'number') {
111 | m = arg;
112 | }
113 | });
114 |
115 | return this.$setState(({ items }) => {
116 | if (isUndefined(m)) {
117 | items.pop();
118 | } else {
119 | items.splice(m, 1);
120 | }
121 |
122 | if (!items.length) {
123 | items = [this.getId()];
124 | }
125 |
126 | return { items };
127 | }, callback);
128 | };
129 |
130 | $setState = (updater, callback) =>
131 | new Promise(resolve =>
132 | this.setState(updater, () =>
133 | this.$formutil.$onValidates($formutil => resolve(runCallback(callback, $formutil)))
134 | )
135 | );
136 |
137 | render() {
138 | const { children, onFocus, onBlur, value } = this.props;
139 | const $self = this;
140 |
141 | if (!isFunction(children)) {
142 | return null;
143 | }
144 |
145 | const $baseutil = {
146 | $insert: this.insert,
147 | $remove: this.remove,
148 | $swap: this.swap,
149 | $push: (values, callback) => this.insert(values, callback),
150 | $pop: callback => this.remove(callback),
151 | $shift: callback => this.remove(0, callback),
152 | $unshift: (values, callback) => this.insert(0, values, callback),
153 | onFocus,
154 | onBlur
155 | };
156 |
157 | return (
158 | component or withForm() HOC, otherwise it's isolated.`
32 | );
33 |
34 | warning($name, `You should assign a name to , otherwise it will be isolated!`);
35 |
36 | if ($formContext.$$register) {
37 | $formContext.$$register($name, this.$fieldHandler);
38 | }
39 |
40 | this.$prevState = this.$state;
41 |
42 | createRef(this.props.$ref, this.$fieldutil);
43 | }
44 |
45 | componentWillUnmount() {
46 | if (this.$formContext.$$unregister) {
47 | this.$formContext.$$unregister(this.props.name, this.$fieldHandler, this.props.$reserveOnUnmount);
48 | }
49 |
50 | this.isMounting = false;
51 |
52 | createRef(this.props.$ref, null);
53 | }
54 |
55 | componentDidUpdate(prevProps) {
56 | const $name = this.props.name;
57 |
58 | if ($name !== prevProps.name) {
59 | if (this.$formContext.$$register) {
60 | this.$formContext.$$register($name, this.$fieldHandler, prevProps.name);
61 | }
62 | }
63 |
64 | createRef(this.props.$ref, this.$fieldutil);
65 |
66 | if (this.$state.$value !== this.$prevState.$value) {
67 | if (!($name in (this.$formContext.$$registers || {}))) {
68 | this.$registered.$$triggerChange({
69 | $newValue: this.$state.$value,
70 | $prevValue: this.$prevState.$value
71 | });
72 | }
73 | }
74 |
75 | this.$prevState = this.$state;
76 | }
77 |
78 | shouldComponentUpdate(nextProps) {
79 | const { $memo } = nextProps;
80 |
81 | return (
82 | !$memo ||
83 | /**
84 | * 这里不能用isEqual深度比较,避免遇到$value为大数据时导致性能问题
85 | * isStateEqual只比较一层
86 | */
87 | !isStateEqual(this.$registered.$getState(), this.$prevState) ||
88 | !(Array.isArray($memo) ? isEqual($memo, this.props.$memo) : isEqual(this.props, nextProps))
89 | );
90 | }
91 |
92 | $setState = ($newState, callback) =>
93 | new Promise(resolve => {
94 | const execute = () => resolve(runCallback(callback, this.$fieldutil));
95 |
96 | if (this.isMounting) {
97 | const $name = this.props.name;
98 |
99 | if ($name in (this.$formContext.$$registers || {})) {
100 | this.shouldRendered = false;
101 | this.$formContext.$$onChange($name, $newState, execute);
102 |
103 | /**
104 | * Ensure Field could rerender if has been cached. In others words, it's vdomEq always true,
105 | * render not trigger Field rerender
106 | */
107 | if (!this.shouldRendered) {
108 | this.forceUpdate();
109 | }
110 | } else {
111 | this.$registered.$$merge($newState);
112 | this.$registered.$$detectChange($newState);
113 |
114 | this.forceUpdate(execute);
115 | }
116 | } else {
117 | this.$registered.$$merge($newState);
118 | execute();
119 | }
120 | });
121 |
122 | _render() {
123 | const $fieldutil = (this.$fieldutil = {
124 | $name: this.props.name,
125 | ...this.$registered.$getState(),
126 | ...this.$registered,
127 | $$formutil: this.$formContext.$formutil
128 | });
129 |
130 | return renderField($fieldutil, this.props);
131 | }
132 |
133 | render() {
134 | this.shouldRendered = true;
135 |
136 | return (
137 |
138 | {getFormContext => {
139 | const shouldInitial = !this.$formContext;
140 |
141 | this.$formContext = getFormContext();
142 |
143 | if (!this.$fieldHandler) {
144 | this.$fieldHandler = createHandler(this, this);
145 | }
146 |
147 | this.$registered =
148 | (this.$formContext.$$registers || {})[this.$fieldHandler.$name] || this.$fieldHandler;
149 |
150 | if (shouldInitial) {
151 | this.$fieldHandler.$$reset();
152 | this.$fieldHandler.$validate();
153 | }
154 |
155 | return this._render();
156 | }}
157 |
158 | );
159 | }
160 | }
161 |
162 | export default Field;
163 |
--------------------------------------------------------------------------------
/src/connect.js:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from 'react';
2 | import hoistStatics from 'hoist-non-react-statics';
3 | import FormContext from './context';
4 |
5 | function connect(WrappedComponent) {
6 | const Connect = forwardRef((props, ref) => {
7 | return (
8 |
9 | {getFormContext => }
10 |
11 | );
12 | });
13 |
14 | Connect.displayName =
15 | 'React.Formutil.connect.' + (WrappedComponent.displayName || WrappedComponent.name || 'Anonymous');
16 |
17 | return hoistStatics(Connect, WrappedComponent);
18 | }
19 |
20 | export default connect;
21 |
--------------------------------------------------------------------------------
/src/context.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | export default createContext(() => ({}));
4 |
--------------------------------------------------------------------------------
/src/fieldHelper.js:
--------------------------------------------------------------------------------
1 | import { Children, cloneElement, createElement } from 'react';
2 | import PropTypes from 'prop-types';
3 | import warning from 'warning';
4 | import * as utils from './utils';
5 | import { FORM_VALIDATE_RESULT } from './Form';
6 |
7 | let FIELD_UUID = 0;
8 | const $baseState = {
9 | $valid: true,
10 | $invalid: false,
11 |
12 | $dirty: false,
13 | $pristine: true,
14 |
15 | $touched: false,
16 | $untouched: true,
17 |
18 | $focused: false,
19 |
20 | $pending: false,
21 |
22 | $error: {}
23 | };
24 |
25 | function isError(result) {
26 | return /*!utils.isUndefined(result) && */ result !== true;
27 | }
28 |
29 | function warningValidatorReturn(result, key, name) {
30 | warning(
31 | !utils.isUndefined(result),
32 | `You should return a string or Error when the validation('${
33 | name && name + ': '
34 | }${key}') failed, otherwise return true.`
35 | );
36 | }
37 |
38 | export const propTypes =
39 | process.env.NODE_ENV !== 'production'
40 | ? {
41 | name: PropTypes.string,
42 |
43 | $defaultValue: PropTypes.any,
44 | $defaultState: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
45 | $onFieldChange: PropTypes.func,
46 | $validators: PropTypes.object,
47 | $asyncValidators: PropTypes.object,
48 | $validateLazy: PropTypes.bool,
49 | $memo: PropTypes.oneOfType([PropTypes.bool, PropTypes.array]),
50 | $reserveOnUnmount: PropTypes.bool,
51 | $ref: PropTypes.oneOfType([
52 | PropTypes.func,
53 | PropTypes.shape({
54 | current: PropTypes.any
55 | })
56 | ]),
57 | $parser: PropTypes.func,
58 | $formatter: PropTypes.func,
59 |
60 | render: PropTypes.func,
61 | component: utils.checkComponentPropType,
62 | children(props, ...args) {
63 | let pt = PropTypes.oneOfType([PropTypes.func, PropTypes.node]);
64 |
65 | if (!props.render && !props.component && props.children !== null) {
66 | pt = pt.isRequired;
67 | }
68 |
69 | return pt(props, ...args);
70 | }
71 | }
72 | : undefined;
73 |
74 | export const displayName = 'React.Formutil.Field';
75 |
76 | export function GET_FIELD_UUID() {
77 | return FIELD_UUID++;
78 | }
79 |
80 | export function renderField($fieldutil, props) {
81 | let { children, render, component } = props;
82 |
83 | if (component) {
84 | return createElement(component, { $fieldutil });
85 | }
86 |
87 | if (utils.isFunction(render)) {
88 | return render($fieldutil);
89 | }
90 |
91 | if (utils.isFunction(children)) {
92 | return children($fieldutil);
93 | }
94 |
95 | return Children.map(children, child =>
96 | child && utils.isComponent(child.type)
97 | ? cloneElement(child, {
98 | $fieldutil
99 | })
100 | : child
101 | );
102 | }
103 |
104 | export function createHandler($this, owner) {
105 | const $fieldHandler = {
106 | $$FIELD_UUID: $this.$$FIELD_UUID,
107 |
108 | $$reset,
109 | $$merge,
110 | $$detectChange,
111 | $$triggerChange,
112 | $onValidate,
113 |
114 | $new() {
115 | return $this.$fieldutil;
116 | },
117 | $picker: $getState,
118 | $getState,
119 | // not support in Hooks
120 | $getComponent() {
121 | return owner;
122 | },
123 |
124 | $reset($state, callback) {
125 | return $this.$setState($$reset($state), callback);
126 | },
127 | $getFirstError,
128 | $validate,
129 | $setState: $this.$setState,
130 | $render,
131 | $setValue,
132 | $setTouched,
133 | $setDirty,
134 | $setFocused,
135 | $setValidity,
136 | $setError,
137 | $setPending
138 | };
139 |
140 | let $$validatePromise;
141 |
142 | function $$detectChange($newState) {
143 | if ('$value' in $newState || '$viewValue' in $newState) {
144 | $validate();
145 | }
146 | }
147 |
148 | function $$triggerChange({ $newValue, $prevValue }) {
149 | const { $onFieldChange } = $this.props;
150 |
151 | if (utils.isFunction($onFieldChange)) {
152 | $onFieldChange($newValue, $prevValue, $this.$formContext.$formutil);
153 | }
154 | }
155 |
156 | function $onValidate(callback) {
157 | $$validatePromise.then(callback);
158 |
159 | return $$validatePromise;
160 | }
161 |
162 | function $$reset($newState) {
163 | let $initialState;
164 |
165 | const { props, $formContext } = $this;
166 |
167 | if ($formContext.$$getDefault) {
168 | const $name = props.name;
169 | const { $$defaultStates, $$defaultValues } = $formContext.$$getDefault();
170 |
171 | if ($name && $$defaultValues) {
172 | const $initialValue = utils.parsePath($$defaultValues, $name);
173 |
174 | $initialState = utils.parsePath($$defaultStates, $name) || {};
175 |
176 | if (!utils.isUndefined($initialValue)) {
177 | $initialState.$value = $initialValue;
178 | }
179 | }
180 | }
181 |
182 | const { $defaultValue, $defaultState } = props;
183 |
184 | return $$merge({
185 | ...$baseState, // the base state
186 | ...(utils.isFunction($defaultState) ? $defaultState(props) : $defaultState), // self default state
187 | $value: utils.isFunction($defaultValue)
188 | ? $defaultValue(props)
189 | : '$defaultValue' in props
190 | ? $defaultValue
191 | : '',
192 | ...$initialState, // the default state from Form
193 | ...$newState
194 | });
195 | }
196 |
197 | function $getState() {
198 | return { ...$this.$state };
199 | }
200 |
201 | function $validate(callback) {
202 | return ($$validatePromise = new Promise(resolve => {
203 | const { props, $formContext } = $this;
204 | const $validators = { ...props.$validators, ...props.$asyncValidators };
205 | const {
206 | $value,
207 | $pending,
208 | $error: { ...$newError }
209 | } = $this.$state;
210 | const { $formutil } = $formContext;
211 | const $validError = {};
212 | let $skipRestValidate = false;
213 | let $breakAsyncHandler;
214 | let $shouldCancelPrevAsyncValidate;
215 | let prevCallback;
216 | let validation;
217 |
218 | delete $newError[FORM_VALIDATE_RESULT];
219 |
220 | const $validatePromises = Object.keys($validators).reduce((promises, key) => {
221 | delete $newError[key];
222 |
223 | if (!$skipRestValidate && props[key] != null) {
224 | const result = $validators[key]($value, props[key], {
225 | ...props,
226 | $formutil,
227 | $fieldutil: $this.$fieldutil,
228 | $validError
229 | });
230 |
231 | if (utils.isPromise(result)) {
232 | promises.push(
233 | // @ts-ignore
234 | result.catch(reason => {
235 | if (!$breakAsyncHandler) {
236 | $setValidity(key, reason || key);
237 | }
238 | })
239 | );
240 | } else if (isError(result)) {
241 | $validError[key] = result || key;
242 |
243 | warningValidatorReturn(result, key, props.name);
244 |
245 | if (props.$validateLazy) {
246 | $skipRestValidate = true;
247 | }
248 | }
249 | }
250 |
251 | return promises;
252 | }, []);
253 | const execCallback = $fieldutil =>
254 | resolve(utils.runCallback(callback, utils.runCallback(prevCallback, $fieldutil)));
255 |
256 | if ($validatePromises.length) {
257 | if (!$pending) {
258 | $setPending(true);
259 | }
260 |
261 | $shouldCancelPrevAsyncValidate = setCallback => ($breakAsyncHandler = setCallback(execCallback));
262 |
263 | $validatePromises.push(
264 | $setError({
265 | ...$newError,
266 | ...$validError
267 | })
268 | );
269 |
270 | validation = Promise.all($validatePromises).then(() => {
271 | if ($breakAsyncHandler) {
272 | return $breakAsyncHandler;
273 | }
274 |
275 | $this.$shouldCancelPrevAsyncValidate = null;
276 |
277 | return $setPending(false, execCallback);
278 | });
279 | } else {
280 | if ($pending) {
281 | $setPending(false);
282 | }
283 |
284 | validation = $setError(
285 | {
286 | ...$newError,
287 | ...$validError
288 | },
289 | execCallback
290 | );
291 | }
292 |
293 | if ($this.$shouldCancelPrevAsyncValidate) {
294 | $this.$shouldCancelPrevAsyncValidate(callback => {
295 | prevCallback = callback;
296 |
297 | return validation;
298 | });
299 | }
300 |
301 | $this.$shouldCancelPrevAsyncValidate = $shouldCancelPrevAsyncValidate;
302 | }));
303 | }
304 |
305 | function $render($viewValue, callback) {
306 | return $this.$setState(
307 | {
308 | $viewValue,
309 | $dirty: true
310 | },
311 | callback
312 | );
313 | }
314 |
315 | function $setValue($value, callback) {
316 | return $this.$setState(
317 | {
318 | $value
319 | },
320 | callback
321 | );
322 | }
323 |
324 | function $setTouched($touched, callback) {
325 | return $this.$setState(
326 | {
327 | $touched
328 | },
329 | callback
330 | );
331 | }
332 |
333 | function $setDirty($dirty, callback) {
334 | return $this.$setState(
335 | {
336 | $dirty
337 | },
338 | callback
339 | );
340 | }
341 |
342 | function $setFocused($focused, callback) {
343 | return $this.$setState(
344 | {
345 | $focused
346 | },
347 | callback
348 | );
349 | }
350 |
351 | function $setError($error, callback) {
352 | return $this.$setState(
353 | {
354 | $error
355 | },
356 | callback
357 | );
358 | }
359 |
360 | function $setValidity(key, result = true, callback) {
361 | const {
362 | $error: { ...$newError }
363 | } = $this.$state;
364 |
365 | if (isError(result)) {
366 | $newError[key] = result || key;
367 |
368 | warningValidatorReturn(result, key, $this.props.name);
369 | } else {
370 | delete $newError[key];
371 | }
372 |
373 | return $setError($newError, callback);
374 | }
375 |
376 | function $setPending($pending, callback) {
377 | return $this.$setState(
378 | {
379 | $pending
380 | },
381 | callback
382 | );
383 | }
384 |
385 | function $getFirstError() {
386 | const { $error = {} } = $this.$state;
387 |
388 | for (let name in $error) {
389 | if ($error.hasOwnProperty(name)) {
390 | return $error[name] instanceof Error ? $error[name].message : $error[name];
391 | }
392 | }
393 | }
394 |
395 | function $$merge({ ...$newState }) {
396 | if ('$error' in $newState) {
397 | if (!$newState.$error) {
398 | $newState.$error = {};
399 | }
400 |
401 | $newState.$valid = Object.keys($newState.$error).length === 0;
402 | }
403 |
404 | // process $value
405 | const { $parser, $formatter } = $this.props;
406 |
407 | if ('$viewValue' in $newState && !('$value' in $newState)) {
408 | const $setViewValue = $value => ($newState.$viewValue = $value);
409 |
410 | $newState.$value = $parser ? $parser($newState.$viewValue, $setViewValue) : $newState.$viewValue;
411 | } else if ('$value' in $newState && !('$viewValue' in $newState)) {
412 | const $setModelValue = $value => ($newState.$value = $value);
413 |
414 | $newState.$viewValue = $formatter ? $formatter($newState.$value, $setModelValue) : $newState.$value;
415 | }
416 |
417 | // process $valid/$invalid
418 | if ('$valid' in $newState) {
419 | $newState.$invalid = !$newState.$valid;
420 | } else if ('$invalid' in $newState) {
421 | $newState.$valid = !$newState.$invalid;
422 | }
423 |
424 | // process $dirty/$pristine
425 | if ('$dirty' in $newState) {
426 | $newState.$pristine = !$newState.$dirty;
427 | } else if ('$pristine' in $newState) {
428 | $newState.$dirty = !$newState.$pristine;
429 | }
430 |
431 | // process $touched/$untouched
432 | if ('$touched' in $newState) {
433 | $newState.$untouched = !$newState.$touched;
434 | } else if ('$untouched' in $newState) {
435 | $newState.$touched = !$newState.$untouched;
436 | }
437 |
438 | $this.$state = { ...$this.$state, ...$newState };
439 |
440 | return $getState();
441 | }
442 |
443 | return $fieldHandler;
444 | }
445 |
--------------------------------------------------------------------------------
/src/hooks/Field.js:
--------------------------------------------------------------------------------
1 | import useField from './useField';
2 | import { propTypes, displayName, renderField } from '../fieldHelper';
3 |
4 | function Field(props) {
5 | const $fieldutil = useField(props);
6 |
7 | return renderField($fieldutil, props);
8 | }
9 |
10 | Field.FieldDisplayName = displayName;
11 | Field.propTypes = propTypes;
12 |
13 | export default Field;
14 |
--------------------------------------------------------------------------------
/src/hooks/useField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import warning from 'warning';
3 | import useFormContext from './useFormContext';
4 | import { runCallback, createRef } from '../utils';
5 | import { createHandler, GET_FIELD_UUID } from '../fieldHelper';
6 |
7 | /**
8 | * @description
9 | * The custom hook for Field
10 | *
11 | * @param {string | object} [name]
12 | * @param {object} [props]
13 | *
14 | * @return {object} $Fieldutil
15 | */
16 | function useField(name, props = {}) {
17 | if (!React.useState) {
18 | throw new Error(`Hooks api need react@>=16.8, Please upgrade your reactjs.`);
19 | }
20 |
21 | const { useState, useLayoutEffect, useEffect, useRef } = React;
22 | const _useEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
23 |
24 | let $name;
25 |
26 | if (name) {
27 | if (typeof name === 'string') {
28 | $name = name;
29 |
30 | props.name = $name;
31 | } else {
32 | props = name;
33 |
34 | $name = props.name;
35 | }
36 | }
37 |
38 | const $formContext = useFormContext();
39 | /** @type {any} */
40 | const $this = useRef({}).current;
41 | /** @type {React.MutableRefObject} */
42 | const callbackRef = useRef([]);
43 |
44 | let $registered;
45 |
46 | $this.$formContext = $formContext;
47 | $this.props = props;
48 | $this.$setState = $setState;
49 | $this.shouldRendered = true;
50 |
51 | // we not directly use this $state, just from $this.$state
52 | const [, setState] = useState(() => {
53 | $this.$$FIELD_UUID = GET_FIELD_UUID();
54 | $this.$fieldHandler = $registered = createHandler($this);
55 |
56 | $this.$fieldHandler.$$reset();
57 | $this.$fieldHandler.$validate();
58 | });
59 |
60 | if (!$registered) {
61 | $registered = ($formContext.$$registers || {})[$this.$fieldHandler.$name] || $this.$fieldHandler;
62 | }
63 |
64 | _useEffect(() => {
65 | const { $state } = $this;
66 |
67 | if ($this.isMounting) {
68 | if (!($name in ($formContext.$$registers || {}))) {
69 | $registered.$$triggerChange({
70 | $newValue: $state.$value,
71 | $prevValue: $this.$prevState.$value
72 | });
73 | }
74 | }
75 |
76 | $this.$prevState = $state;
77 | // eslint-disable-next-line
78 | }, [$this.$state.$value]);
79 |
80 | _useEffect(() => {
81 | $this.isMounting = true;
82 |
83 | warning(
84 | !$name || $formContext.$formutil,
85 | `You should enusre that the useField() with the name '${$name}' must be used underneath a component or withForm() HOC, otherwise it's isolated.`
86 | );
87 |
88 | warning($name, `You should pass a name argument to useField(), otherwise it will be isolated!`);
89 |
90 | return () => {
91 | $this.isMounting = false;
92 |
93 | createRef(props.$ref, null);
94 | };
95 | // eslint-disable-next-line
96 | }, []);
97 |
98 | _useEffect(() => {
99 | if ($formContext.$$register) {
100 | $formContext.$$register($name, $this.$fieldHandler);
101 | }
102 |
103 | return () => {
104 | if ($formContext.$$unregister) {
105 | $formContext.$$unregister($name, $this.$fieldHandler, !$this.isMounting && props.$reserveOnUnmount);
106 | }
107 | };
108 | // eslint-disable-next-line
109 | }, [$name]);
110 |
111 | // trigger ref callback
112 | _useEffect(() => {
113 | createRef(props.$ref, $this.$fieldutil);
114 | });
115 |
116 | _useEffect(() => {
117 | if (callbackRef.current.length > 0) {
118 | const callbackQueue = [...callbackRef.current];
119 |
120 | callbackRef.current.length = 0;
121 |
122 | while (callbackQueue.length) {
123 | callbackQueue.pop()($this.$fieldutil);
124 | }
125 | }
126 | });
127 |
128 | function $setState($newState, callback) {
129 | return new Promise(resolve => {
130 | const execute = () => resolve(runCallback(callback, $this.$fieldutil));
131 |
132 | if ($this.isMounting) {
133 | if ($name in ($formContext.$$registers || {})) {
134 | $this.shouldRendered = false;
135 | $formContext.$$onChange($name, $newState, execute);
136 |
137 | if (!$this.shouldRendered) {
138 | setState({});
139 | }
140 | } else {
141 | $registered.$$merge($newState);
142 | $registered.$$detectChange($newState);
143 |
144 | setState({});
145 |
146 | callbackRef.current.push(execute);
147 | }
148 | } else {
149 | $registered.$$merge($newState);
150 | execute();
151 | }
152 | });
153 | }
154 |
155 | return ($this.$fieldutil = {
156 | $name,
157 | ...$registered.$getState(),
158 | ...$registered,
159 | $$formutil: $formContext.$formutil
160 | });
161 | }
162 |
163 | export default useField;
164 |
--------------------------------------------------------------------------------
/src/hooks/useForm.js:
--------------------------------------------------------------------------------
1 | import useFormContext from './useFormContext';
2 |
3 | function useForm() {
4 | const { $formutil } = useFormContext();
5 |
6 | return $formutil;
7 | }
8 |
9 | export default useForm;
10 |
--------------------------------------------------------------------------------
/src/hooks/useFormContext.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import FormContext from '../context';
3 |
4 | function useFormContext() {
5 | if (!React.useState) {
6 | throw new Error(`Hooks api need react@>=16.8, Please upgrade your reactjs.`);
7 | }
8 |
9 | const { useContext } = React;
10 | const getFormContext = useContext(FormContext);
11 |
12 | return getFormContext();
13 | }
14 |
15 | export default useFormContext;
16 |
--------------------------------------------------------------------------------
/src/hooks/useHandler.js:
--------------------------------------------------------------------------------
1 | import { createHandler, parseProps, defaultProps } from '../EasyField/easyFieldHandler';
2 | import useField from './useField';
3 |
4 | function useHandler(props) {
5 | props = { ...defaultProps, ...props, children: null };
6 |
7 | const { fieldProps, childProps } = parseProps(props);
8 | const $fieldutil = useField(fieldProps);
9 |
10 | return createHandler($fieldutil, fieldProps, childProps);
11 | }
12 |
13 | export default useHandler;
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { default as Form } from './Form';
2 | export { default as withForm } from './withForm';
3 | export { default as Field } from './Field';
4 | export { default as withField } from './withField';
5 | export { default as EasyField } from './EasyField';
6 | export { default as connect } from './connect';
7 | export { default as formContext } from './context';
8 |
9 | // hooks
10 | export { default as useField } from './hooks/useField';
11 | export { default as useForm } from './hooks/useForm';
12 | export { default as useHandler } from './hooks/useHandler';
13 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | import warning from 'warning';
2 | import reactIs from 'react-is';
3 |
4 | const { isValidElementType } = reactIs;
5 |
6 | const OBJECT_PROTO = Object.getPrototypeOf({});
7 | const PATH_REGEXP = /\s*(?:\]\s*\.|\]\s*\[|\.|\[|\])\s*/g;
8 | const Root = typeof window === 'undefined' ? global : window;
9 |
10 | export function isUndefined(arg) {
11 | return typeof arg === 'undefined';
12 | }
13 |
14 | export function isFunction(arg) {
15 | return typeof arg === 'function';
16 | }
17 |
18 | export function isEmpty(arg) {
19 | return isUndefined(arg) || arg === null || arg + '' === '';
20 | }
21 |
22 | export function isPromise(promise) {
23 | return !!promise && isFunction(promise.then);
24 | }
25 |
26 | export function isObject(obj) {
27 | return Object.prototype.toString.call(obj) === '[object Object]';
28 | }
29 |
30 | export function isPlainObj(obj) {
31 | if (!isObject(obj)) return false;
32 | if (null === Object.getPrototypeOf(obj)) return true;
33 | if (!isFunction(obj.constructor)) return false;
34 |
35 | return obj.constructor.prototype === OBJECT_PROTO;
36 | }
37 |
38 | export function isComponent(obj) {
39 | return isValidElementType(obj) && typeof obj !== 'string';
40 | }
41 |
42 | export function checkComponentPropType(props, propName, componentName) {
43 | if (props[propName] && !isComponent(props[propName])) {
44 | return new Error(
45 | `Invalid prop 'component' supplied to '${componentName}': the prop is not a valid React component`
46 | );
47 | }
48 | }
49 |
50 | // quick clone deeply
51 | export function deepClone(obj) {
52 | if (Array.isArray(obj)) {
53 | const newObj = [];
54 |
55 | for (let i = 0, j = obj.length; i < j; i++) {
56 | newObj[i] = deepClone(obj[i]);
57 | }
58 |
59 | return newObj;
60 | } else if (isPlainObj(obj)) {
61 | const newObj = {};
62 |
63 | for (let i in obj) {
64 | if (obj.hasOwnProperty(i)) newObj[i] = deepClone(obj[i]);
65 | }
66 |
67 | return newObj;
68 | }
69 |
70 | return obj;
71 | }
72 |
73 | export const runCallback = function(callback, ...args) {
74 | if (isFunction(callback)) {
75 | callback(...args);
76 | }
77 |
78 | return args[0];
79 | };
80 |
81 | export function createHOC(withHOC) {
82 | return function(...args) {
83 | if (isComponent(args[0])) {
84 | return withHOC(...args);
85 | }
86 |
87 | return function(WrappedComponent) {
88 | return withHOC(WrappedComponent, args[0]);
89 | };
90 | };
91 | }
92 |
93 | const VALID_PROPS = ['minlength', 'maxlength', 'max', 'min', 'required', 'pattern', 'step'];
94 |
95 | export function isValidProp(prop) {
96 | return VALID_PROPS.indexOf(prop.toLowerCase()) > -1;
97 | }
98 |
99 | /* eslint-disable */
100 | const executeWord = function(word) {
101 | try {
102 | const exec = new Function(
103 | 'origin',
104 | 'global',
105 | `return typeof ${word} === 'number' || (typeof ${word} !== 'undefined' && !(origin in global)) ? ${word} : origin`
106 | );
107 | return exec(word, Root);
108 | } catch (err) {
109 | return word;
110 | }
111 | };
112 |
113 | /**
114 | * @desc 解析表达式中赋值深路径对象
115 | *
116 | * @param {object} target 要赋值的对象
117 | * @param {string} path 赋值路径,eg:list[0].title
118 | * @param {any} [value] 要赋过去的值,如过不传,则返回解析路径后的值
119 | *
120 | * 使用示例:parsePath({}, 'list[0].authors[1].name', 'Lucy');
121 | */
122 | export function parsePath(...args) {
123 | const [target, path, value] = args;
124 |
125 | warning(typeof path === 'string', `The second parameter(${JSON.stringify(path)}) of parsePath() must be a string.`);
126 |
127 | const pathSymbols = (path.match(PATH_REGEXP) || []).map(s => s.replace(/\s/g, ''));
128 | const pathWords = path
129 | .split(PATH_REGEXP)
130 | .map(s => s.trim())
131 | .filter(item => item !== '');
132 | let scope = target;
133 |
134 | try {
135 | if (args.length < 3) {
136 | for (let index = 0, len = pathWords.length; index < len; index++) {
137 | const word = executeWord(pathWords[index]);
138 |
139 | if (index + 1 === len) {
140 | return scope[word];
141 | }
142 |
143 | if (isUndefined(scope[word])) {
144 | break;
145 | }
146 |
147 | scope = scope[word];
148 | }
149 | } else {
150 | for (let index = 0, length = pathWords.length; index < length; index++) {
151 | const word = executeWord(pathWords[index]);
152 | const nextWord = pathWords[index + 1];
153 | const symbol = pathSymbols[index];
154 |
155 | if (isUndefined(nextWord)) {
156 | scope[word] = value;
157 | break;
158 | }
159 |
160 | switch (symbol) {
161 | case '].':
162 | case '.':
163 | scope = scope[word] = isUndefined(scope[word]) ? {} : { ...scope[word] };
164 | break;
165 |
166 | case '][':
167 | case '[':
168 | const nextVarWord = executeWord(nextWord);
169 |
170 | scope = scope[word] = isUndefined(scope[word])
171 | ? typeof nextVarWord === 'number' && nextVarWord >= 0
172 | ? []
173 | : {}
174 | : Array.isArray(scope[word])
175 | ? [...scope[word]]
176 | : { ...scope[word] };
177 | break;
178 |
179 | default:
180 | scope[word] = value;
181 | break;
182 | }
183 | }
184 | }
185 | } catch (error) {
186 | warning(false, `The name '%s' of Field seems is not a legal expression.`, path);
187 | }
188 |
189 | if (args.length > 2) {
190 | return target;
191 | }
192 | }
193 |
194 | export function pathExist(scope, path) {
195 | const pathWords = path
196 | .split(PATH_REGEXP)
197 | .map(s => s.trim())
198 | .filter(item => item !== '');
199 |
200 | for (let index = 0, len = pathWords.length; index < len; index++) {
201 | const word = executeWord(pathWords[index]);
202 |
203 | if (!(word in scope)) {
204 | break;
205 | }
206 |
207 | if (index + 1 === len) {
208 | return {
209 | data: scope[word]
210 | };
211 | }
212 |
213 | scope = scope[word];
214 | }
215 | }
216 |
217 | export function createRef(ref, value) {
218 | if (ref) {
219 | if (isFunction(ref)) {
220 | ref(value);
221 | } else if ('current' in ref) {
222 | ref.current = value;
223 | }
224 | }
225 | }
226 |
227 | export const arrayFind = (array, process) => {
228 | for (let i = 0, j = array.length; i < j; i++) {
229 | if (process(array[i]) === true) {
230 | return array[i];
231 | }
232 | }
233 | };
234 |
235 | export const objectMap = (obj, handler) =>
236 | Object.keys(obj).reduce((newObj, key) => {
237 | newObj[key] = handler(obj[key], key, obj);
238 | return newObj;
239 | }, {});
240 |
241 | export const objectEach = (obj, handler) => Object.keys(obj).forEach(key => handler(obj[key], key, obj));
242 |
243 | export const toObject = (arr, handler, obj = {}) =>
244 | arr.reduce((...args) => {
245 | handler(...args);
246 |
247 | return args[0];
248 | }, obj);
249 |
250 | const TODO_DELETE = undefined;
251 | export function CLEAR(obj, pkey, pobj) {
252 | objectEach(obj, (value, key) => {
253 | if (value === TODO_DELETE) {
254 | delete obj[key];
255 | } else if (isPlainObj(value) || Array.isArray(value)) {
256 | CLEAR(value, key, obj);
257 | }
258 | });
259 |
260 | if (pobj && Object.keys(obj).every(key => obj[key] === TODO_DELETE)) {
261 | pobj[pkey] = TODO_DELETE;
262 | CLEAR(pobj);
263 | }
264 | }
265 | export const objectClear = (obj, name) => {
266 | if (!isUndefined(parsePath(obj, name))) {
267 | parsePath(obj, name, TODO_DELETE);
268 |
269 | CLEAR(obj);
270 | }
271 | };
272 |
273 | export function isStateEqual(prev, next) {
274 | if (prev === next) {
275 | return true;
276 | }
277 |
278 | const keys = Object.keys(prev);
279 |
280 | if (keys.length !== Object.keys(next).length) {
281 | return false;
282 | }
283 |
284 | for (let i = 0; i < keys.length; i++) {
285 | if (prev[keys[i]] !== next[keys[i]]) {
286 | return false;
287 | }
288 | }
289 |
290 | return true;
291 | }
292 |
--------------------------------------------------------------------------------
/src/withField.js:
--------------------------------------------------------------------------------
1 | import React, { Component, forwardRef } from 'react';
2 | import hoistStatics from 'hoist-non-react-statics';
3 | import Field from './Field';
4 | import { createHOC } from './utils';
5 |
6 | const filterProps = [
7 | 'name',
8 |
9 | '$defaultValue',
10 | '$defaultState',
11 | '$onFieldChange',
12 | '$validators',
13 | '$asyncValidators',
14 | '$validateLazy',
15 | '$memo',
16 | '$reserveOnUnmount',
17 | '$ref',
18 | '$parser',
19 | '$formatter',
20 |
21 | 'render',
22 | 'component',
23 | 'children'
24 | ];
25 |
26 | function withField(WrappedComponent, config = {}) {
27 | class WithField extends Component {
28 | static displayName =
29 | 'React.Formutil.withField.' + (WrappedComponent.displayName || WrappedComponent.name || 'Anonymous');
30 |
31 | renderChildren = $fieldutil => (
32 |
33 | );
34 |
35 | render() {
36 | const { __forwardRef__, ...others } = this.props;
37 | // component优先级最高,这里排除掉, 避免和render属性冲突
38 | const { component, ...fieldProps } = this.props;
39 |
40 | filterProps
41 | .concat(
42 | Object.keys({
43 | ...config.$validators,
44 | ...config.$asyncValidators,
45 | ...others.$validators,
46 | ...others.$asyncValidators
47 | })
48 | )
49 | .forEach(prop => {
50 | if (prop in others) {
51 | if (prop === '$validators' || prop === '$asyncValidators' || prop === '$defaultState') {
52 | fieldProps[prop] = { ...config[prop], ...others[prop] };
53 | }
54 |
55 | delete others[prop];
56 | }
57 | });
58 |
59 | this.othersProps = others;
60 |
61 | return ;
62 | }
63 | }
64 |
65 | const ForwardRefField = forwardRef((props, ref) => );
66 |
67 | ForwardRefField.displayName =
68 | 'React.Formutil.withField.ForwardRef.' + (WrappedComponent.displayName || WrappedComponent.name || 'Anonymous');
69 |
70 | return hoistStatics(ForwardRefField, WrappedComponent);
71 | }
72 |
73 | export default createHOC(withField);
74 |
--------------------------------------------------------------------------------
/src/withForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component, forwardRef } from 'react';
2 | import hoistStatics from 'hoist-non-react-statics';
3 | import Form from './Form';
4 | import { createHOC } from './utils';
5 |
6 | const filterProps = [
7 | 'render',
8 | 'component',
9 | 'children',
10 | '$defaultValues',
11 | '$defaultStates',
12 | '$onFormChange',
13 | '$validator',
14 | '$processer',
15 | '$ref'
16 | ];
17 |
18 | function withForm(WrappedComponent, config = {}) {
19 | class WithForm extends Component {
20 | static displayName =
21 | 'React.Formutil.withForm.' + (WrappedComponent.displayName || WrappedComponent.name || 'Anonymous');
22 |
23 | renderChildren = $formutil => (
24 |
25 | );
26 |
27 | render() {
28 | const { __forwardRef__, ...others } = this.props;
29 | // component优先级最高,这里排除掉, 避免和render属性冲突
30 | const { component, ...formProps } = this.props;
31 |
32 | filterProps.forEach(prop => {
33 | if (prop in others) {
34 | if (prop === '$defaultStates' || prop === '$defaultValues') {
35 | formProps[prop] = { ...config[prop], ...others[prop] };
36 | }
37 |
38 | delete others[prop];
39 | }
40 | });
41 |
42 | this.othersProps = others;
43 |
44 | return ;
45 | }
46 | }
47 |
48 | const ForwardRefForm = forwardRef((props, ref) => );
49 |
50 | ForwardRefForm.displayName =
51 | 'React.Formutil.withForm.ForwardRef.' + (WrappedComponent.displayName || WrappedComponent.name || 'Anonymous');
52 |
53 | return hoistStatics(ForwardRefForm, WrappedComponent);
54 | }
55 |
56 | export default createHOC(withForm);
57 |
--------------------------------------------------------------------------------
/tests/connect.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect, EasyField } from '../src';
3 | import { renderForm } from './helper';
4 |
5 | const ConnectForm = connect(props => {
6 | props.formRef?.(props.$formutil);
7 |
8 | return ;
9 | });
10 |
11 | test('should pass $formutil', async () => {
12 | let innerFormutil;
13 | const { getFormutil } = renderForm( (innerFormutil = f)} />);
14 |
15 | expect(getFormutil()).toBe(innerFormutil.$new());
16 | });
17 |
--------------------------------------------------------------------------------
/tests/dev.dist.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import { $Formutil } from '..';
4 | import { Form as FormCJS, EasyField as EasyFieldCJS } from '../dist/react-formutil.cjs.development';
5 | import { Form as FormESM, EasyField as EasyFieldESM } from '../dist/react-formutil.esm.development';
6 | import { Form as FormUMD, EasyField as EasyFieldUMD } from '../dist/react-formutil.umd.development';
7 |
8 | test('react-formutil.cjs.production should running', async () => {
9 | let formutilRef = React.createRef<$Formutil>();
10 |
11 | render(
12 |
13 | {() => (
14 | <>
15 |
16 |
17 | >
18 | )}
19 |
20 | );
21 |
22 | expect(formutilRef.current!.$params).toEqual({
23 | a: 1,
24 | b: {
25 | c: 2
26 | }
27 | });
28 | });
29 |
30 | test('react-formutil.esm.production should running', async () => {
31 | let formutilRef = React.createRef<$Formutil>();
32 |
33 | render(
34 |
35 | {() => (
36 | <>
37 |
38 |
39 | >
40 | )}
41 |
42 | );
43 |
44 | expect(formutilRef.current!.$params).toEqual({
45 | a: 1,
46 | b: {
47 | c: 2
48 | }
49 | });
50 | });
51 |
52 | test('react-formutil.umd.production should running', async () => {
53 | let formutilRef = React.createRef<$Formutil>();
54 |
55 | render(
56 |
57 | {() => (
58 | <>
59 |
60 |
61 | >
62 | )}
63 |
64 | );
65 |
66 | expect(formutilRef.current!.$params).toEqual({
67 | a: 1,
68 | b: {
69 | c: 2
70 | }
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/tests/helper.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import { $Formutil, FormProps, $Fieldutil, FieldProps } from '../index.d';
4 | import { Form, Field } from '../src';
5 |
6 | export function renderForm(
7 | content: React.ReactNode | (($formutil: $Formutil) => React.ReactNode),
8 | formProps?: FormProps
9 | ) {
10 | let formHandler: $Formutil;
11 | const getForm = content => (
12 |
19 | );
20 | const { rerender, ...rest } = render(getForm(content));
21 |
22 | return {
23 | getFormutil() {
24 | return formHandler;
25 | },
26 | ...rest,
27 | rerender: (newContent?: any) => rerender(getForm(newContent === undefined ? content : newContent))
28 | };
29 | }
30 |
31 | export function renderField(fieldProps?: FieldProps) {
32 | let fieldHandler: $Fieldutil;
33 | let formHandler: $Formutil;
34 | let instance;
35 | const getForm = (newProps?: FieldProps) => (
36 |
72 | );
73 | const { rerender, ...rest } = render(getForm());
74 |
75 | return {
76 | getFieldutil() {
77 | return fieldHandler;
78 | },
79 | getFormutil() {
80 | return formHandler;
81 | },
82 | getInstance() {
83 | return instance;
84 | },
85 | getElement() {
86 | return rest.getByTestId('input') as HTMLInputElement;
87 | },
88 | rerender(newProps?: FieldProps) {
89 | return rerender(getForm(newProps));
90 | },
91 | ...rest
92 | };
93 | }
94 |
--------------------------------------------------------------------------------
/tests/prod.dist.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import { $Formutil } from '..';
4 | import { Form as FormCJS, EasyField as EasyFieldCJS } from '../dist/react-formutil.cjs.production';
5 | import { Form as FormESM, EasyField as EasyFieldESM } from '../dist/react-formutil.esm.production';
6 | import { Form as FormUMD, EasyField as EasyFieldUMD } from '../dist/react-formutil.umd.production';
7 |
8 | test('react-formutil.cjs.production should running', async () => {
9 | let formutilRef = React.createRef<$Formutil>();
10 |
11 | render(
12 |
13 | {() => (
14 | <>
15 |
16 |
17 | >
18 | )}
19 |
20 | );
21 |
22 | expect(formutilRef.current!.$params).toEqual({
23 | a: 1,
24 | b: {
25 | c: 2
26 | }
27 | });
28 | });
29 |
30 | test('react-formutil.esm.production should running', async () => {
31 | let formutilRef = React.createRef<$Formutil>();
32 |
33 | render(
34 |
35 | {() => (
36 | <>
37 |
38 |
39 | >
40 | )}
41 |
42 | );
43 |
44 | expect(formutilRef.current!.$params).toEqual({
45 | a: 1,
46 | b: {
47 | c: 2
48 | }
49 | });
50 | });
51 |
52 | test('react-formutil.umd.production should running', async () => {
53 | let formutilRef = React.createRef<$Formutil>();
54 |
55 | render(
56 |
57 | {() => (
58 | <>
59 |
60 |
61 | >
62 | )}
63 |
64 | );
65 |
66 | expect(formutilRef.current!.$params).toEqual({
67 | a: 1,
68 | b: {
69 | c: 2
70 | }
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/tests/useField.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import userEvent from '@testing-library/user-event';
3 | import { useField } from '../src';
4 | import { renderForm } from './helper';
5 |
6 | function UseFieldCompoennt(props) {
7 | const $fieldutil = useField(props);
8 |
9 | return (
10 | $fieldutil.$render(ev.target.value)} />
11 | );
12 | }
13 |
14 | test('should handle input field', () => {
15 | const { getFormutil, getByTestId } = renderForm();
16 |
17 | expect(getFormutil().$params).toEqual({
18 | a: '1'
19 | });
20 |
21 | userEvent.type(getByTestId('input'), '2');
22 |
23 | expect(getFormutil().$params).toEqual({
24 | a: '12'
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/tests/utils.test.ts:
--------------------------------------------------------------------------------
1 | import * as utils from '../src/utils';
2 |
3 | test('parsePath() set value', () => {
4 | expect(utils.parsePath({}, 'a.b', true)).toEqual({
5 | a: { b: true }
6 | });
7 |
8 | expect(utils.parsePath({}, 'a[b]', true)).toEqual({
9 | a: { b: true }
10 | });
11 |
12 | expect(utils.parsePath({}, 'a["b"]', true)).toEqual({
13 | a: { b: true }
14 | });
15 |
16 | expect(utils.parsePath({}, 'a[1]', true)).toEqual({
17 | a: [undefined, true]
18 | });
19 |
20 | expect(utils.parsePath({}, 'a[1 + 1]', true)).toEqual({
21 | a: [undefined, undefined, true]
22 | });
23 |
24 | expect(utils.parsePath({}, 'a[1 + "a"]', true)).toEqual({
25 | a: {
26 | '1a': true
27 | }
28 | });
29 |
30 | expect(utils.parsePath({}, 'a.b[c][0][0].d', true)).toEqual({
31 | a: {
32 | b: {
33 | c: [
34 | [
35 | {
36 | d: true
37 | }
38 | ]
39 | ]
40 | }
41 | }
42 | });
43 | });
44 |
45 | test('parsePath() get value', () => {
46 | expect(utils.parsePath({ a: { b: true } }, 'a.b')).toEqual(true);
47 |
48 | expect(utils.parsePath({ a: { b: true } }, 'a[b]')).toEqual(true);
49 |
50 | expect(utils.parsePath({ a: { b: true } }, 'a["b"]')).toEqual(true);
51 |
52 | expect(
53 | utils.parsePath(
54 | {
55 | a: [undefined, true]
56 | },
57 | 'a[1]'
58 | )
59 | ).toEqual(true);
60 |
61 | expect(utils.parsePath({ a: [undefined, undefined, true] }, 'a[1 + 1]')).toEqual(true);
62 |
63 | expect(
64 | utils.parsePath(
65 | {
66 | a: {
67 | '1a': true
68 | }
69 | },
70 | 'a[1 + "a"]'
71 | )
72 | ).toEqual(true);
73 |
74 | expect(
75 | utils.parsePath(
76 | {
77 | a: {
78 | b: {
79 | c: [
80 | [
81 | {
82 | d: true
83 | }
84 | ]
85 | ]
86 | }
87 | }
88 | },
89 | 'a.b[c][0][0].d'
90 | )
91 | ).toEqual(true);
92 | });
93 |
94 | test('isStateEqual()', () => {
95 | expect(
96 | utils.isStateEqual(
97 | {
98 | a: true
99 | },
100 | {
101 | a: true
102 | }
103 | )
104 | ).toBe(true);
105 |
106 | expect(
107 | utils.isStateEqual(
108 | {
109 | a: true
110 | },
111 | {
112 | a: true,
113 | b: true
114 | }
115 | )
116 | ).toBe(false);
117 |
118 | expect(
119 | utils.isStateEqual(
120 | {
121 | a: {}
122 | },
123 | {
124 | a: {}
125 | }
126 | )
127 | ).toBe(false);
128 | });
129 |
130 | test('objectClear()', () => {
131 | const obj = {
132 | a: {
133 | b: {
134 | c: [
135 | [
136 | {
137 | d: true
138 | }
139 | ]
140 | ]
141 | }
142 | }
143 | };
144 |
145 | utils.objectClear(obj, 'a.b[c][0][0].d');
146 |
147 | expect(obj).toEqual({});
148 |
149 | const obj1 = {
150 | a: {
151 | b: {
152 | c: [
153 | [
154 | {
155 | e: true
156 | }
157 | ]
158 | ],
159 | d: true
160 | }
161 | }
162 | };
163 |
164 | utils.objectClear(obj1, 'a.b[c][0][0].e');
165 |
166 | expect(obj1).toEqual({
167 | a: {
168 | b: {
169 | d: true
170 | }
171 | }
172 | });
173 | });
174 |
--------------------------------------------------------------------------------
/tests/withField.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import userEvent from '@testing-library/user-event';
3 | import { withField } from '../src';
4 | import { renderForm } from './helper';
5 |
6 | const CustomField = withField(({ $fieldutil, ...others }) => {
7 | return $fieldutil.$render(ev.target.value)} />;
8 | });
9 |
10 | test('should pass $fieldutil', async () => {
11 | expect(CustomField.displayName).toBe('React.Formutil.withField.ForwardRef.Anonymous');
12 |
13 | const { getFormutil, getByTestId } = renderForm();
14 |
15 | expect(getFormutil().$params).toEqual({
16 | a: '1'
17 | });
18 |
19 | userEvent.type(getByTestId('input'), '2');
20 |
21 | expect(getFormutil().$params).toEqual({
22 | a: '12'
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/tests/withForm.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import { withForm, EasyField } from '../src';
4 | import { $Formutil } from '../index.d';
5 |
6 | function FormPage() {
7 | return (
8 |
9 |
10 |
11 |
12 | );
13 | }
14 |
15 | test('withForm', () => {
16 | const formutilRef = React.createRef<$Formutil>();
17 | const Form = withForm(FormPage);
18 |
19 | expect(Form.displayName).toBe('React.Formutil.withForm.ForwardRef.FormPage');
20 |
21 | render();
22 |
23 | expect(formutilRef.current!.$params).toEqual({
24 | a: '',
25 | b: ''
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES5",
4 | "module": "ESNext",
5 | "lib": ["esnext", "dom"],
6 | "jsx": "react",
7 | "resolveJsonModule": true,
8 | "experimentalDecorators": true,
9 | "allowSyntheticDefaultImports": true,
10 | "allowUnreachableCode": false,
11 | "moduleResolution": "node",
12 | "forceConsistentCasingInFileNames": true,
13 | "noImplicitReturns": true,
14 | "skipLibCheck": true,
15 | "noImplicitThis": true,
16 | "noImplicitAny": false,
17 | "strictNullChecks": true,
18 | "noUnusedLocals": true,
19 | "importHelpers": true
20 | },
21 | "exclude": ["node_modules", "scripts", "dist", "build", "buildDev"]
22 | }
23 |
--------------------------------------------------------------------------------