├── .gitignore ├── packages ├── boot │ ├── tsconfig.json │ ├── __tests__ │ │ └── boot.test.js │ ├── tsconfig.prod.json │ ├── src │ │ ├── definePage.tsx │ │ ├── helpers.ts │ │ ├── index.ts │ │ ├── defineServices.ts │ │ ├── defineStore.ts │ │ ├── Collector.tsx │ │ ├── global.ts │ │ ├── runApp.tsx │ │ └── defineComponent.tsx │ ├── scripts │ │ ├── webpack.config.dev.js │ │ ├── pack.js │ │ └── get-webpack-config.js │ ├── package.json │ ├── README.md │ └── CHANGELOG.md └── request │ ├── .eslintrc │ ├── tsconfig.json │ ├── tsconfig.prod.json │ ├── src │ ├── index.ts │ ├── global-config.ts │ ├── helpers.ts │ ├── create-service.ts │ └── request.ts │ ├── README.md │ ├── package.json │ ├── __tests__ │ └── request.test.ts │ └── CHANGELOG.md ├── examples └── basic-example │ ├── src │ ├── stores │ │ ├── index.js │ │ ├── counter.js │ │ └── dogs.js │ ├── components │ │ ├── index.js │ │ ├── button.js │ │ ├── json-view.js │ │ └── input.js │ ├── routes.js │ ├── pages │ │ ├── fun.js │ │ └── index.js │ ├── index.js │ └── services │ │ └── index.js │ ├── .eslintrc │ ├── package.json │ ├── index.html │ └── webpack.config.js ├── .prettierrc ├── babel.config.js ├── .eslintrc ├── lerna.json ├── tsconfig.json ├── tsconfig.prod.json ├── LICENSE ├── package.json ├── README.md └── jest.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | dist/ 4 | lib/ 5 | -------------------------------------------------------------------------------- /packages/boot/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } -------------------------------------------------------------------------------- /packages/request/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint-config-ali/typescript"] 3 | } -------------------------------------------------------------------------------- /packages/request/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | } 4 | -------------------------------------------------------------------------------- /examples/basic-example/src/stores/index.js: -------------------------------------------------------------------------------- 1 | import './counter'; 2 | import './dogs'; 3 | -------------------------------------------------------------------------------- /packages/boot/__tests__/boot.test.js: -------------------------------------------------------------------------------- 1 | it('test', () => { 2 | expect(1).toBe(1); 3 | }); 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": true, 4 | "printWidth": 100 5 | } 6 | -------------------------------------------------------------------------------- /examples/basic-example/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint-config-ali/react"], 3 | "globals": { 4 | "tango": "readonly" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/basic-example/src/components/index.js: -------------------------------------------------------------------------------- 1 | export { Button } from './button'; 2 | export { Input } from './input'; 3 | export { JsonView } from './json-view'; 4 | -------------------------------------------------------------------------------- /packages/boot/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.prod.json", 3 | "compilerOptions": { 4 | "outDir": "./lib" 5 | }, 6 | "include": ["src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/request/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.prod.json", 3 | "compilerOptions": { 4 | "outDir": "./lib" 5 | }, 6 | "include": ["src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/request/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './request'; 2 | export * from './request'; 3 | export * from './create-service'; 4 | export { default as globalConfig } from './global-config'; 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * BabelConfig for Jest 3 | */ 4 | module.exports = { 5 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-react', '@babel/preset-typescript'], 6 | }; 7 | -------------------------------------------------------------------------------- /examples/basic-example/src/components/button.js: -------------------------------------------------------------------------------- 1 | import { defineComponent } from '@music163/tango-boot'; 2 | import { Button as AntButton } from 'antd'; 3 | 4 | export const Button = defineComponent(AntButton, { 5 | name: 'Button', 6 | }); 7 | -------------------------------------------------------------------------------- /examples/basic-example/src/components/json-view.js: -------------------------------------------------------------------------------- 1 | import ReactJson from 'react-json-view'; 2 | import { view } from '@music163/tango-boot'; 3 | 4 | export const JsonView = view((props) => { 5 | return ; 6 | }); 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint-config-ali/typescript/react"], 3 | "ignorePatterns": [ 4 | "**/dist/**/*", 5 | "**/lib/**/*", 6 | "**/node_modules/**/*", 7 | "scripts/**/*" 8 | ], 9 | "rules": { 10 | "prefer-destructuring": "off" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/basic-example/src/stores/counter.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from '@music163/tango-boot'; 2 | 3 | defineStore( 4 | { 5 | number: 10, 6 | 7 | add() { 8 | this.number++; 9 | }, 10 | 11 | decrement() { 12 | this.number--; 13 | }, 14 | }, 15 | 'counter', 16 | ); 17 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "useWorkspaces": true, 4 | "version": "independent", 5 | "packages": ["packages/*"], 6 | "npmClient": "yarn", 7 | "command": { 8 | "version": { 9 | "message": "chore(release): publish" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/boot/src/definePage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { store, view } from '@risingstack/react-easy-state'; 3 | import globalTango from './global'; 4 | 5 | export function definePage(BaseComponent: React.ComponentType) { 6 | globalTango.page = store({}); 7 | const Page = view(BaseComponent); 8 | return Page; 9 | } 10 | -------------------------------------------------------------------------------- /examples/basic-example/src/routes.js: -------------------------------------------------------------------------------- 1 | import Index from './pages/index'; 2 | import Fun from './pages/fun'; 3 | 4 | const routes = [ 5 | { 6 | path: '/', 7 | exact: true, 8 | component: Index, 9 | }, 10 | { 11 | path: '/fun', 12 | exact: true, 13 | component: Fun, 14 | }, 15 | ]; 16 | 17 | export default routes; 18 | -------------------------------------------------------------------------------- /examples/basic-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-example", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "webpack serve", 7 | "build": "webpack" 8 | }, 9 | "dependencies": { 10 | "@music163/antd": "^0.2.4", 11 | "@music163/tango-boot": "*", 12 | "react-json-view": "^1.21.3" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/boot/scripts/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const getWebpackConfig = require('./get-webpack-config'); 3 | 4 | module.exports = getWebpackConfig( 5 | { 6 | devtool: 'inline-source-map', 7 | devServer: { 8 | static: path.join(__dirname, '../dist'), 9 | port: 9001, 10 | }, 11 | }, 12 | {} 13 | ); 14 | -------------------------------------------------------------------------------- /examples/basic-example/src/pages/fun.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { definePage } from '@music163/tango-boot'; 3 | import { Button } from '../components'; 4 | 5 | function App() { 6 | return ( 7 |
8 |

hello world

9 | 10 |
11 | ); 12 | } 13 | 14 | export default definePage(App); 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.prod.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "noEmit": true, 6 | "sourceMap": true, 7 | "resolveJsonModule": true, 8 | "strictFunctionTypes": true, 9 | "paths": { 10 | "@music163/tango-boot": ["packages/boot/src/index.ts"], 11 | "@music163/request": ["packages/request/src/index.ts"], 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/basic-example/src/index.js: -------------------------------------------------------------------------------- 1 | import { runApp } from '@music163/tango-boot'; 2 | import routes from './routes'; 3 | import './stores'; 4 | import './services'; 5 | 6 | // window.__TANGO_DESIGNER__ = { 7 | // version: '1.0.0', 8 | // }; 9 | 10 | runApp({ 11 | boot: { 12 | mountElement: document.querySelector('#root'), 13 | qiankun: false, 14 | }, 15 | router: { 16 | type: 'hash', 17 | config: routes, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /packages/boot/src/helpers.ts: -------------------------------------------------------------------------------- 1 | 2 | export function mergeRefs(...refs: any[]) { 3 | const filteredRefs = refs.filter(Boolean); 4 | if (!filteredRefs.length) return null; 5 | if (filteredRefs.length === 0) return filteredRefs[0]; 6 | return (instance: any) => { 7 | for (const ref of filteredRefs) { 8 | if (typeof ref === 'function') { 9 | ref(instance); 10 | } else if (ref) { 11 | ref.current = instance; 12 | } 13 | } 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /packages/boot/src/index.ts: -------------------------------------------------------------------------------- 1 | import globalTango from './global'; 2 | 3 | // browser 4 | if (typeof window !== 'undefined') { 5 | (window as any).tango = globalTango; 6 | } 7 | 8 | export default globalTango; 9 | 10 | export * from './definePage'; 11 | export * from './defineServices'; 12 | export * from './defineStore'; 13 | export * from './defineComponent'; 14 | export * from './runApp'; 15 | export * from './Collector'; 16 | 17 | export * from '@risingstack/react-easy-state'; 18 | -------------------------------------------------------------------------------- /examples/basic-example/src/services/index.js: -------------------------------------------------------------------------------- 1 | import { defineServices } from '@music163/tango-boot'; 2 | 3 | // service 默认会提取响应中的 data 字段作为返回数据,此处需对不符合预期的原始请求响应做转换。 4 | const formatRes = (res) => ({ data: { ...res } }); 5 | 6 | export default defineServices({ 7 | list: { 8 | url: 'https://dog.ceo/api/breeds/list/all', 9 | formatter: formatRes, 10 | }, 11 | 12 | getBreed: { 13 | url: 'https://dog.ceo/api/breed/:breed/images/random', 14 | formatter: formatRes, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /examples/basic-example/src/stores/dogs.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from '@music163/tango-boot'; 2 | 3 | defineStore( 4 | { 5 | list: {}, 6 | 7 | image: '', 8 | 9 | async listAllBreeds() { 10 | const ret = await tango.services.list(); 11 | this.list = ret.message; 12 | }, 13 | 14 | async getRandomImage(name) { 15 | const ret = await tango.services.getBreed(null, { 16 | pathVariables: { 17 | breed: name, 18 | }, 19 | }); 20 | this.image = ret.message; 21 | }, 22 | }, 23 | 'dogs', 24 | ); 25 | -------------------------------------------------------------------------------- /packages/boot/src/defineServices.ts: -------------------------------------------------------------------------------- 1 | import { createServices, RequestConfig } from '@music163/request'; 2 | import globalTango from './global'; 3 | 4 | interface IDefineServicesBaseConfig extends RequestConfig { 5 | namespace?: string; 6 | } 7 | 8 | export function defineServices( 9 | configs: Record, 10 | baseConfig?: IDefineServicesBaseConfig, 11 | ) { 12 | const { namespace, ...restConfig } = baseConfig || {}; 13 | const services = createServices(configs, restConfig); 14 | globalTango.registerServices(services, namespace); 15 | return services; 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "module": "ES2015", 5 | "lib": ["dom", "esnext"], 6 | "importHelpers": true, 7 | "declaration": true, 8 | "sourceMap": false, 9 | "moduleResolution": "node", 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "suppressImplicitAnyIndexErrors": true, 14 | "ignoreDeprecations": "5.0", 15 | "allowSyntheticDefaultImports": true, 16 | "strictNullChecks": false, 17 | "jsx": "react", 18 | "esModuleInterop": true 19 | }, 20 | "exclude": ["node_modules", "dist"] 21 | } 22 | -------------------------------------------------------------------------------- /packages/boot/scripts/pack.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const getWebpackConfig = require('./get-webpack-config'); 3 | 4 | const args = process.argv.slice(2); 5 | const minimize = args.indexOf('minimize') > -1; 6 | 7 | const config = getWebpackConfig( 8 | {}, 9 | { 10 | minimize, 11 | }, 12 | ); 13 | 14 | webpack(config, (err, stats) => { 15 | if (err || stats.hasErrors()) { 16 | console.log(err); 17 | // handle error 18 | throw err; 19 | } 20 | 21 | console.info( 22 | stats.toString({ 23 | colors: true, 24 | chunks: false, 25 | modules: false, 26 | hash: false, 27 | usedExports: false, 28 | version: false, 29 | }), 30 | ); 31 | }); 32 | -------------------------------------------------------------------------------- /examples/basic-example/src/components/input.js: -------------------------------------------------------------------------------- 1 | import { defineComponent } from '@music163/tango-boot'; 2 | import { Input as AntInput } from 'antd'; 3 | 4 | export const Input = defineComponent(AntInput, { 5 | name: 'Input', 6 | registerState: { 7 | getInitStates({ setPageState }, props) { 8 | return { 9 | value: props.defaultValue ?? '', 10 | clear() { 11 | setPageState({ value: '' }); 12 | }, 13 | setValue(nextValue) { 14 | setPageState({ value: nextValue }); 15 | }, 16 | }; 17 | }, 18 | 19 | getTriggerProps({ setPageState, getPageState }) { 20 | return { 21 | value: getPageState()?.value, 22 | onChange(e) { 23 | setPageState({ value: e.target.value }); 24 | }, 25 | }; 26 | }, 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /packages/request/README.md: -------------------------------------------------------------------------------- 1 | # `request` 2 | 3 | > A simple axios wrapper for better async data fetching 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import request, { createService, createServices } from '@music163/request'; 9 | ``` 10 | 11 | ## request 12 | 13 | ```js 14 | request(url, configs); 15 | 16 | request.get(url, params, configs); 17 | 18 | request.post(url, data, configs); 19 | ``` 20 | 21 | request config documentation: 22 | 23 | ## createService 24 | 25 | ```js 26 | const listAll = createService({ 27 | url: '/api/listAll', 28 | }); 29 | 30 | const list = await listAll(payload, configs); 31 | ``` 32 | 33 | ## createServices 34 | 35 | ```js 36 | const services = createServices({ 37 | listAll: { 38 | url: '/api/listAll', 39 | }, 40 | }); 41 | 42 | const list = await services.listAll(payload, configs); 43 | ``` 44 | -------------------------------------------------------------------------------- /packages/boot/src/defineStore.ts: -------------------------------------------------------------------------------- 1 | import { store } from '@risingstack/react-easy-state'; 2 | import globalTango from './global'; 3 | 4 | // export const globalStore = store({}); 5 | 6 | /** 7 | * 创建 Store 实例 8 | * @param object 定义的状态熟悉和方法 9 | * @param namespace 命名空间,暂时废弃,不推荐使用 10 | * @returns 11 | */ 12 | export function defineStore(object: any, namespace: string) { 13 | // if (namespace) { 14 | // globalStore[namespace] = object; 15 | // return globalStore[namespace]; 16 | // // return bind(globalStore[namespace]); 17 | // } 18 | const ret = bind(store(object)); 19 | globalTango.registerStore(namespace, ret); 20 | return ret; 21 | } 22 | 23 | function bind(object: any) { 24 | Object.keys(object).forEach((key) => { 25 | if (typeof object[key] === 'function') { 26 | object[key] = object[key].bind(object); 27 | } 28 | }); 29 | return object; 30 | } 31 | -------------------------------------------------------------------------------- /packages/request/src/global-config.ts: -------------------------------------------------------------------------------- 1 | const MESSAGE = Symbol('message'); 2 | 3 | const message = { 4 | error(msg: string) { 5 | console.error('[error] %s', msg); 6 | }, 7 | warn(msg: string) { 8 | console.warn('[warn] %s', msg); 9 | }, 10 | success(msg: string) { 11 | console.log('[success] %s', msg); 12 | }, 13 | }; 14 | 15 | class GlobalConfig { 16 | constructor() { 17 | this[MESSAGE] = message; 18 | } 19 | 20 | get message() { 21 | return this[MESSAGE]; 22 | } 23 | 24 | set message(messageInstance) { 25 | if ( 26 | messageInstance && 27 | messageInstance.error && 28 | messageInstance.warn && 29 | messageInstance.success 30 | ) { 31 | this[MESSAGE] = message; 32 | } else { 33 | console.error('invalid message, should have error, warn, success method'); 34 | } 35 | } 36 | } 37 | 38 | export default new GlobalConfig(); 39 | -------------------------------------------------------------------------------- /examples/basic-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | example 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 NetEase 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 | -------------------------------------------------------------------------------- /packages/request/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@music163/request", 3 | "version": "0.2.1", 4 | "description": "a request library based on axios", 5 | "author": "wwsun ", 6 | "homepage": "https://github.com/music163/tango-boot#readme", 7 | "license": "MIT", 8 | "main": "lib/cjs/index.js", 9 | "module": "lib/esm/index.js", 10 | "types": "lib/esm/index.d.ts", 11 | "scripts": { 12 | "clean": "rimraf dist/ && rimraf lib/", 13 | "build": "yarn clean && yarn build:esm && yarn build:cjs", 14 | "build:esm": "tsc --project tsconfig.prod.json --outDir lib/esm/ --module ES2020", 15 | "build:cjs": "tsc --project tsconfig.prod.json --outDir lib/cjs/ --module CommonJS", 16 | "prepublishOnly": "yarn build" 17 | }, 18 | "files": [ 19 | "dist", 20 | "lib" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/music163/tango-boot.git" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/music163/tango-boot/issues" 28 | }, 29 | "dependencies": { 30 | "axios": "^1.7.2" 31 | }, 32 | "publishConfig": { 33 | "access": "public", 34 | "registry": "https://registry.npmjs.org/" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/request/src/helpers.ts: -------------------------------------------------------------------------------- 1 | function hasKey(obj: any, key: string) { 2 | return Object.prototype.hasOwnProperty.call(obj, key); 3 | } 4 | 5 | function isSimpleObject(obj: any) { 6 | return ( 7 | typeof obj === 'object' && 8 | obj !== null && 9 | !Array.isArray(obj) && 10 | !(obj instanceof Date) && 11 | !(obj instanceof RegExp) 12 | ); 13 | } 14 | 15 | export function mergeObjects(obj1: object, obj2: object) { 16 | if (!obj1) { 17 | return obj2; 18 | } 19 | 20 | if (!obj2) { 21 | return obj1; 22 | } 23 | 24 | const result: object = {}; 25 | 26 | for (const key in obj1) { 27 | if (hasKey(obj1, key)) { 28 | if (isSimpleObject(obj1[key]) && isSimpleObject(obj2[key])) { 29 | result[key] = mergeObjects(obj1[key], obj2[key]) as any; 30 | } else { 31 | result[key] = obj1[key]; 32 | } 33 | } 34 | } 35 | 36 | for (const key in obj2) { 37 | if (hasKey(obj2, key)) { 38 | if (isSimpleObject(obj2[key]) && !hasKey(result, key)) { 39 | result[key] = mergeObjects({}, obj2[key]) as any; 40 | } else if (!hasKey(result, key)) { 41 | result[key] = obj2[key]; 42 | } 43 | } 44 | } 45 | 46 | return result; 47 | } 48 | -------------------------------------------------------------------------------- /packages/request/__tests__/request.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * API documentation 3 | * @see https://dog.ceo/dog-api/documentation/ 4 | */ 5 | 6 | import request, { createService, createServices } from '../src'; 7 | 8 | it('should request', async () => { 9 | const data = await request('https://dog.ceo/api/breeds/list/all'); 10 | expect(data.message).toBeTruthy(); 11 | }); 12 | 13 | it('should request with payload', async () => { 14 | const data = await request.get('https://animechan.xyz/api/random/anime', { 15 | title: 'naruto', 16 | }); 17 | expect(data.anime).toBeTruthy(); 18 | }); 19 | 20 | it('should formatter', async () => { 21 | const data = await request.get('https://dog.ceo/api/breeds/list/all', null, { 22 | formatter: (res) => { 23 | console.log(res); 24 | return { 25 | data: res.message, 26 | }; 27 | }, 28 | }); 29 | expect(data.data).toBeTruthy(); 30 | }); 31 | 32 | it('should createService', async () => { 33 | const listAll = createService({ 34 | url: 'https://dog.ceo/api/breeds/list/all', 35 | }); 36 | const data = await listAll(); 37 | expect(data.message).toBeTruthy(); 38 | }); 39 | 40 | it('should createServices', async () => { 41 | const services = createServices({ 42 | listAll: { 43 | url: 'https://dog.ceo/api/breeds/list/all', 44 | }, 45 | }); 46 | const data = await services.listAll(); 47 | 48 | expect(data.message).toBeTruthy(); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/request/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.2.1](https://github.com/music163/tango-boot/compare/@music163/request@0.2.0...@music163/request@0.2.1) (2024-05-31) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * init router history & set default to browser history ([#3](https://github.com/music163/tango-boot/issues/3)) ([c3a1f81](https://github.com/music163/tango-boot/commit/c3a1f8129036fb86ad24c360943af6902dc893fe)) 12 | 13 | 14 | 15 | 16 | 17 | # [0.2.0](https://github.com/music163/tango-boot/compare/@music163/request@0.1.2...@music163/request@0.2.0) (2024-03-06) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * fix method ([1b4feea](https://github.com/music163/tango-boot/commit/1b4feea9ec4bb2474e451320665046213935f332)) 23 | * onSuccess and onError in service handler ([b46b509](https://github.com/music163/tango-boot/commit/b46b509f3de5a6033cce65815e6ed04ed6c084a5)) 24 | 25 | 26 | ### Features 27 | 28 | * refactor request core ([3385ab6](https://github.com/music163/tango-boot/commit/3385ab6ae3fa1bc9324b348cb308289e350e5b94)) 29 | 30 | 31 | 32 | 33 | 34 | ## [0.1.2](https://github.com/music163/tango-boot/compare/@music163/request@0.1.1...@music163/request@0.1.2) (2023-09-05) 35 | 36 | 37 | ### Bug Fixes 38 | 39 | * add pathVariables support ([7cc3dcb](https://github.com/music163/tango-boot/commit/7cc3dcb4f30a9e7972e2dff608a1873a239b7977)) 40 | -------------------------------------------------------------------------------- /packages/request/src/create-service.ts: -------------------------------------------------------------------------------- 1 | import request, { RequestConfig } from './request'; 2 | import globalConfig from './global-config'; 3 | 4 | export interface CreateServiceConfig extends RequestConfig { 5 | onSuccess?: (data: any) => any; 6 | onError?: (error: any) => any; 7 | } 8 | 9 | export function createService({ onSuccess, onError, ...baseConfig }: CreateServiceConfig) { 10 | return async (payload?: any, config?: RequestConfig) => { 11 | const { 12 | method = 'get', 13 | url, 14 | ...restConfig 15 | } = { 16 | ...config, 17 | ...baseConfig, 18 | }; 19 | try { 20 | const fn = fixMethod(method); 21 | const data = await request[fn](url, payload, restConfig); 22 | onSuccess?.(data); 23 | return data; 24 | } catch (err) { 25 | globalConfig.message?.error((err as any).message); 26 | onError?.(err); 27 | } 28 | }; 29 | } 30 | 31 | export function createServices( 32 | configs: Record, 33 | baseConfig?: RequestConfig, 34 | ): Record> { 35 | return Object.keys(configs).reduce((acc, key) => { 36 | acc[key] = createService({ ...configs[key], ...baseConfig }); 37 | return acc; 38 | }, {}); 39 | } 40 | 41 | const methodMap = { 42 | postformdata: 'postFormData', 43 | postformurlencoded: 'postFormUrlencoded', 44 | }; 45 | 46 | function fixMethod(method: string) { 47 | method = method.toLowerCase(); 48 | return methodMap[method] || method; 49 | } 50 | -------------------------------------------------------------------------------- /packages/boot/scripts/get-webpack-config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ProgressBarPlugin = require('progress-bar-webpack-plugin'); 3 | 4 | module.exports = (configs = {}, options = {}) => { 5 | const baseConfig = { 6 | mode: 'development', 7 | devtool: 'eval-source-map', 8 | entry: './src/index.ts', 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.tsx?$/, 13 | use: { 14 | loader: 'babel-loader', 15 | options: { 16 | presets: [ 17 | ['@babel/preset-env', { modules: false }], 18 | '@babel/preset-typescript', 19 | '@babel/preset-react', 20 | ], 21 | }, 22 | }, 23 | exclude: /node_modules/, 24 | }, 25 | ], 26 | }, 27 | plugins: [new ProgressBarPlugin()], 28 | resolve: { 29 | extensions: ['.tsx', '.ts', '.js'], 30 | }, 31 | externals: { 32 | react: 'React', 33 | 'react-dom': 'ReactDOM', 34 | }, 35 | output: { 36 | filename: 'boot.js', 37 | path: path.resolve(__dirname, '../dist/'), 38 | publicPath: '', // relative to HTML page (same directory) 39 | library: 'TangoBoot', 40 | libraryTarget: 'umd', 41 | umdNamedDefine: true, 42 | }, 43 | }; 44 | 45 | if (options.minimize) { 46 | baseConfig.mode = 'production'; 47 | baseConfig.devtool = false; 48 | baseConfig.output.filename = 'boot.min.js'; 49 | } 50 | 51 | return { 52 | ...baseConfig, 53 | ...configs, 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /examples/basic-example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: './src/index.js', 6 | output: { 7 | filename: 'main.js', 8 | path: path.resolve(__dirname, 'dist'), 9 | // 微前端支持 10 | // library: `example-[name]`, 11 | // libraryTarget: 'umd', 12 | // jsonpFunction: `webpackJsonp_example`, 13 | }, 14 | mode: 'development', 15 | devtool: 'inline-source-map', 16 | devServer: { 17 | static: './dist', 18 | headers: { 19 | 'Access-Control-Allow-Origin': '*', 20 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 21 | 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization', 22 | }, 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.m?js$/, 28 | exclude: /node_modules/, 29 | use: { 30 | loader: 'babel-loader', 31 | options: { 32 | presets: ['@babel/preset-env', '@babel/preset-react'], 33 | }, 34 | }, 35 | }, 36 | { 37 | test: /\.html$/, 38 | loader: 'html-loader', 39 | }, 40 | ], 41 | }, 42 | externals: { 43 | react: 'React', 44 | 'react-dom': 'ReactDOM', 45 | moment: 'moment', 46 | 'styled-components': 'styled', 47 | '@music163/tango-boot': 'TangoBoot', 48 | '@music163/antd': 'TangoAntd', 49 | }, 50 | plugins: [ 51 | new HtmlWebpackPlugin({ 52 | title: 'webpack example', 53 | template: 'index.html', 54 | inject: 'body', 55 | }), 56 | ], 57 | }; 58 | -------------------------------------------------------------------------------- /packages/boot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@music163/tango-boot", 3 | "version": "0.3.5", 4 | "description": "a react app framework", 5 | "author": "wwsun ", 6 | "homepage": "https://github.com/music163/tango-boot#readme", 7 | "license": "MIT", 8 | "main": "lib/cjs/index.js", 9 | "module": "lib/esm/index.js", 10 | "types": "lib/esm/index.d.ts", 11 | "scripts": { 12 | "clean": "rimraf dist/ && rimraf lib/", 13 | "build": "yarn clean && yarn build:esm && yarn build:cjs && yarn build:umd", 14 | "build:esm": "tsc --project tsconfig.prod.json --outDir lib/esm/ --module ES2020", 15 | "build:cjs": "tsc --project tsconfig.prod.json --outDir lib/cjs/ --module CommonJS", 16 | "build:umd": "node ./scripts/pack.js", 17 | "serve": "webpack serve --config ./scripts/webpack.config.dev.js", 18 | "prepublishOnly": "yarn build" 19 | }, 20 | "files": [ 21 | "dist", 22 | "lib" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/music163/tango-boot.git" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/music163/tango-boot/issues" 30 | }, 31 | "peerDependencies": { 32 | "history": "4", 33 | "react": ">= 16.8", 34 | "react-router": "5" 35 | }, 36 | "dependencies": { 37 | "@music163/request": "^0.2.1", 38 | "@music163/tango-helpers": "^1.1.1", 39 | "@risingstack/react-easy-state": "^6.3.0", 40 | "@types/react-router-config": "^5.0.11", 41 | "history": "^4.10.1", 42 | "react-error-boundary": "^4.0.11", 43 | "react-router": "5", 44 | "react-router-config": "5" 45 | }, 46 | "devDependencies": { 47 | "@types/react-router": "^5.1.20" 48 | }, 49 | "publishConfig": { 50 | "access": "public", 51 | "registry": "https://registry.npmjs.org/" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/boot/README.md: -------------------------------------------------------------------------------- 1 | # TangoBoot 2 | 3 | TangoBoot is a frontend framework that serves the NetEase Tango Low-Code applications. It provides standard data requests, state management, and routing solutions, as well as generic runtime utility functions, allowing developers to generate Single Page Applications through less codes. 4 | 5 | Documentation: 6 | 7 | ## Usage 8 | 9 | ```jsx 10 | import { runApp, definePage, defineStore, defineServices } from '@music163/boot'; 11 | ``` 12 | 13 | ### defineStore 14 | 15 | define a reactive store object 16 | 17 | ```jsx 18 | import { defineStore } from '@music163/tango-boot'; 19 | 20 | defineStore( 21 | { 22 | number: 10, 23 | 24 | add() { 25 | this.number++; 26 | }, 27 | 28 | decrement() { 29 | this.number--; 30 | }, 31 | }, 32 | 'counter', 33 | ); 34 | ``` 35 | 36 | ### defineServices 37 | 38 | define a collection of async functions 39 | 40 | ```jsx 41 | import { defineServices } from '@music163/tango-boot'; 42 | 43 | export default defineServices({ 44 | list: { 45 | url: 'https://dog.ceo/api/breeds/list/all', 46 | }, 47 | }); 48 | ``` 49 | 50 | ### definePage 51 | 52 | define a observable page component 53 | 54 | ```jsx 55 | import React from 'react'; 56 | import { definePage } from '@music163/tango-boot'; 57 | 58 | class App extends React.Component { 59 | render() { 60 | return ( 61 |
62 |

{tango.stores.counter.number}

63 | 64 | 65 |
66 | ); 67 | } 68 | } 69 | 70 | export default definePage(App); 71 | ``` 72 | 73 | ### runApp 74 | 75 | ```jsx 76 | runApp({ 77 | boot: { 78 | mountElement: document.querySelector('#root'), 79 | }, 80 | routes: [ 81 | { 82 | path: '/', 83 | exact: true, 84 | component: IndexPage, 85 | }, 86 | ], 87 | }); 88 | ``` 89 | -------------------------------------------------------------------------------- /packages/boot/src/Collector.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { view } from '@risingstack/react-easy-state'; 3 | import globalTango from './global'; 4 | 5 | export interface CollectorRenderProps { 6 | value: any; 7 | setValue: (nextValue: any) => void; 8 | ref: React.Ref; 9 | } 10 | 11 | export interface CollectorProps { 12 | id?: string; 13 | model?: string; 14 | defaultValue?: any; 15 | onValueChange?: (value: any) => void; 16 | render?: (props: CollectorRenderProps) => React.ReactNode; 17 | children?: (props: CollectorRenderProps) => React.ReactNode; 18 | } 19 | 20 | // TODO: 支持同步 value 以外的状态, setValue 完全交给外部来做,内部只实例化空间 21 | export const Collector = view( 22 | ({ id, model, defaultValue, onValueChange, render, children }: CollectorProps) => { 23 | useEffect(() => { 24 | const storePath = `currentPage.${id}`; 25 | if (id) { 26 | globalTango.setStoreValue(storePath, { 27 | value: defaultValue, 28 | }); 29 | } 30 | return () => { 31 | if (id) { 32 | globalTango.clearStoreValue(storePath); 33 | } 34 | }; 35 | }, [id]); 36 | 37 | const valuePath = model || ['currentPage', id, 'value'].join('.'); 38 | const value = globalTango.getStoreValue(valuePath); 39 | const componentOrFunction = render || children; 40 | return renderProps(componentOrFunction, { 41 | value, 42 | setValue: (nextValue: any) => { 43 | globalTango.setStoreValue(valuePath, nextValue); 44 | onValueChange?.(nextValue); 45 | }, 46 | ref: (instance: any) => { 47 | globalTango.refs[id] = instance; 48 | }, 49 | }); 50 | }, 51 | ); 52 | 53 | const renderProps = (ComponentOrFunction: any, props: Record) => { 54 | if (ComponentOrFunction.propTypes || ComponentOrFunction.prototype.render) { 55 | return ; 56 | } 57 | return ComponentOrFunction({ 58 | ...(ComponentOrFunction.defaultProps || {}), 59 | ...props, 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boot", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*", 6 | "examples/*" 7 | ], 8 | "scripts": { 9 | "start": "concurrently \"yarn dev\" \"yarn serve:boot\"", 10 | "dev": "yarn workspace basic-example dev", 11 | "build": "lerna run build", 12 | "build:boot": "yarn workspace @music163/tango-boot build", 13 | "serve:boot": "yarn workspace @music163/tango-boot serve", 14 | "test": "jest", 15 | "test:watch": "jest --watch", 16 | "eslint": "eslint packages/**/src/*.{ts,tsx}", 17 | "publish": "lerna publish", 18 | "ver": "lerna version --no-private --conventional-commits", 19 | "release": "yarn eslint && yarn build && yarn run ver && lerna publish from-git", 20 | "release:beta": "yarn eslint && yarn build && yarn run ver && lerna publish from-package --dist-tag beta", 21 | "up": "yarn upgrade-interactive --latest" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.22.10", 25 | "@babel/eslint-parser": "^7.22.10", 26 | "@babel/preset-env": "^7.19.3", 27 | "@babel/preset-react": "^7.16.7", 28 | "@babel/preset-typescript": "^7.21.0", 29 | "@types/jest": "^29.5.3", 30 | "@types/react": "^18.2.20", 31 | "@types/react-dom": "^18.2.7", 32 | "@typescript-eslint/eslint-plugin": "^6.4.0", 33 | "@typescript-eslint/parser": "^6.4.0", 34 | "babel-loader": "^9.1.3", 35 | "concurrently": "^7.6.0", 36 | "eslint": "^8.47.0", 37 | "eslint-config-ali": "^14.0.2", 38 | "eslint-import-resolver-typescript": "^3.6.0", 39 | "eslint-plugin-import": "^2.28.0", 40 | "eslint-plugin-react": "^7.33.1", 41 | "eslint-plugin-react-hooks": "^4.6.0", 42 | "html-loader": "^4.2.0", 43 | "html-webpack-plugin": "^5.5.0", 44 | "jest": "^29.6.2", 45 | "jest-environment-jsdom": "^29.6.2", 46 | "lerna": "^6.5.1", 47 | "progress-bar-webpack-plugin": "2", 48 | "react": "^18.2.0", 49 | "react-dom": "^18.2.0", 50 | "rimraf": "^3.0.2", 51 | "typescript": "^5.1.6", 52 | "webpack": "^5.38.1", 53 | "webpack-cli": "^4.7.2", 54 | "webpack-dev-server": "^4.13.1" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/basic-example/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { definePage } from '@music163/tango-boot'; 3 | import { Layout, Card, Text, Box } from '@music163/antd'; 4 | import { Input, Button } from '../components'; 5 | 6 | class App extends React.Component { 7 | render() { 8 | return ( 9 | 10 | 13 | 14 | hello 15 | 16 | 17 |

{tango.stores.counter.number}

18 | 19 | 20 |
21 | 22 | 23 |
输入框的值为:{tango.pageStore.input1?.value}
24 | 32 |
33 | 34 | 41 |
42 |
43 | {Object.keys(tango.stores.dogs?.list || {}).map((item) => ( 44 | 53 | ))} 54 |
55 | {tango.stores.dogs?.image ? ( 56 |
57 | breed image 58 |
59 | ) : ( 60 |
点击上面的 dog name,加载图片
61 | )} 62 |
63 |
64 |
65 | ); 66 | } 67 | } 68 | 69 | export default definePage(App); 70 | -------------------------------------------------------------------------------- /packages/boot/src/global.ts: -------------------------------------------------------------------------------- 1 | import { getValue, setValue } from '@music163/tango-helpers'; 2 | import { store } from '@risingstack/react-easy-state'; 3 | 4 | type StoreType = ReturnType; 5 | 6 | const globalTango = { 7 | config: {}, 8 | services: {}, 9 | stores: { 10 | currentPage: store({}), 11 | }, 12 | page: {}, 13 | refs: {}, 14 | 15 | history: null as any, 16 | 17 | /** 18 | * 获取当前页面的状态集合 19 | */ 20 | get pageStore() { 21 | return globalTango.page; 22 | }, 23 | 24 | getStore(name: string): StoreType { 25 | return globalTango.stores[name]; 26 | }, 27 | 28 | getStoreValue(path: string) { 29 | if (!path) { 30 | return; 31 | } 32 | 33 | const keys = path.split('.'); 34 | let value = globalTango.stores; 35 | for (let i = 0; i < keys.length; i++) { 36 | if (value) { 37 | value = value[keys[i]]; 38 | } else { 39 | break; 40 | } 41 | } 42 | return value; 43 | }, 44 | 45 | setStoreValue(path: string, value: any) { 46 | const keys = path.split('.'); 47 | const storeName = keys[0]; 48 | const subStore = globalTango.stores[storeName]; 49 | if (!subStore) { 50 | globalTango.stores[storeName] = {}; 51 | } 52 | let context = globalTango.stores[storeName]; 53 | for (let i = 1; i < keys.length - 1; i++) { 54 | context = context[keys[i]] || {}; 55 | } 56 | context[keys[keys.length - 1]] = value; 57 | }, 58 | 59 | clearStoreValue(path: string) { 60 | globalTango.setStoreValue(path, undefined); 61 | }, 62 | 63 | registerStore(name: string, storeInstance: StoreType) { 64 | if (!globalTango.getStore(name)) { 65 | globalTango.stores[name] = storeInstance; 66 | } 67 | }, 68 | 69 | getPageState(name: string) { 70 | return getValue(globalTango.page, name); 71 | }, 72 | 73 | setPageState(name: string, value: any) { 74 | if (!name) { 75 | return; 76 | } 77 | const nextValue = { 78 | ...globalTango.getPageState(name), 79 | ...value, 80 | }; 81 | setValue(globalTango.page, name, nextValue); 82 | }, 83 | 84 | clearPageState(name: string) { 85 | if (!name) { 86 | this.page = {}; 87 | } 88 | delete this.page[name]; 89 | }, 90 | 91 | registerServices(services: any, namespace?: string) { 92 | if (namespace) { 93 | globalTango.services[namespace] = services; 94 | } else { 95 | globalTango.services = { 96 | ...globalTango.services, 97 | ...services, 98 | }; 99 | } 100 | }, 101 | 102 | navigateTo(routePath: string) { 103 | if (globalTango.history) { 104 | globalTango.history.push(routePath); 105 | } else { 106 | window.history.pushState({}, '', routePath); 107 | } 108 | }, 109 | }; 110 | 111 | export default globalTango; 112 | -------------------------------------------------------------------------------- /packages/request/src/request.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import type { AxiosInstance, AxiosRequestConfig } from 'axios'; 3 | import { mergeObjects } from './helpers'; 4 | 5 | export interface RequestConfig extends AxiosRequestConfig { 6 | /** 7 | * 响应格式化函数 8 | * @param data 响应数据 9 | * @returns 返回格式化后的数据 10 | */ 11 | formatter?: (data: any) => any; 12 | /** 13 | * 路径变量,例如 `/users/:id`,其中 id 为路径变量 14 | */ 15 | pathVariables?: Record; 16 | } 17 | 18 | export interface RequestInstance extends AxiosInstance { 19 | get: (url: string, params?: object, config?: RequestConfig) => Promise; 20 | postFormData: (url: string, data?: object, config?: RequestConfig) => Promise; 21 | postFormUrlencoded: (url: string, data?: object, config?: RequestConfig) => Promise; 22 | } 23 | 24 | function createInstance(instanceConfig?: AxiosRequestConfig) { 25 | // 避免多个实例产生冲突 26 | const instance = axios.create(instanceConfig) as RequestInstance; 27 | 28 | // request 拦截器 29 | instance.interceptors.request.use( 30 | (config) => { 31 | const { pathVariables } = config as RequestConfig; 32 | if (pathVariables) { 33 | config.url = config.url.replace(/\/:(\w+)/gi, (_, key) => `/${pathVariables[key]}`); 34 | } 35 | return config; 36 | }, 37 | (error) => { 38 | return Promise.reject(error); 39 | }, 40 | ); 41 | 42 | // response 拦截器 43 | instance.interceptors.response.use( 44 | (response) => { 45 | let data = response.data; 46 | const { formatter } = response.config as RequestConfig; 47 | if (formatter) { 48 | data = formatter(data); 49 | } 50 | 51 | /** 52 | * data 格式 53 | * { 54 | * code: 200, // 必选,业务状态码 55 | * message: 'success', // 必选,业务状态描述 56 | * data: {} | [], // 必选,业务数据 57 | * } 58 | */ 59 | const { code = 200, message } = data; 60 | 61 | if (code !== 200) { 62 | throw new Error(`${code}: ${message || 'request failed!'}, ${response.config.url}`); 63 | } 64 | 65 | if (!('data' in data)) { 66 | console.warn('response data should have a data field', response.config.url); 67 | } 68 | 69 | return data.data; 70 | }, 71 | (error) => { 72 | return Promise.reject(error); 73 | }, 74 | ); 75 | 76 | // 扩展 get 方法 77 | instance.get = (url: string, params?: Record, config?: RequestConfig) => { 78 | return instance.request({ url, params, ...config }); 79 | }; 80 | 81 | // 扩展 post 方法 82 | instance.postFormData = (url: string, data?: object, config?: RequestConfig) => { 83 | return instance.post( 84 | url, 85 | data, 86 | mergeObjects(config, { 87 | headers: { 88 | 'Content-Type': 'multipart/form-data', 89 | }, 90 | }), 91 | ); 92 | }; 93 | 94 | // 扩展 post 方法 95 | instance.postFormUrlencoded = (url: string, data?: object, config?: RequestConfig) => { 96 | return instance.post( 97 | url, 98 | data, 99 | mergeObjects(config, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }), 100 | ); 101 | }; 102 | 103 | return instance; 104 | } 105 | 106 | export default createInstance(); 107 | -------------------------------------------------------------------------------- /packages/boot/src/runApp.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Router } from 'react-router'; 4 | import { RouteConfig, renderRoutes } from 'react-router-config'; 5 | import { History, createBrowserHistory, createHashHistory } from 'history'; 6 | import globalTango from './global'; 7 | 8 | export interface RunAppConfig { 9 | /** 10 | * 启动项配置 11 | */ 12 | boot: { 13 | mountElement: HTMLElement; 14 | qiankun: false | { appName: string }; 15 | }; 16 | /** 17 | * 单个页面应用的根组件,适合不需要前端路由的情况 18 | */ 19 | singleEntry?: React.ComponentType; 20 | /** 21 | * 多页应用路由配置, router.config 的快捷写法 22 | */ 23 | routes: RouteConfig[]; 24 | /** 25 | * 路由配置 26 | */ 27 | router?: { 28 | config: RouteConfig[]; 29 | type?: 'browser' | 'hash'; 30 | basename?: string; 31 | }; 32 | /** 33 | * 根组件的父容器设置,传入的组件将一次作为应用根节点的父级组件 34 | */ 35 | providers?: AppContainerProps['providers']; 36 | } 37 | 38 | export function runApp(config: RunAppConfig) { 39 | if ((window as any).__POWERED_BY_QIANKUN__ && config.boot.qiankun) { 40 | runQiankunApp(config); 41 | } else { 42 | runReactApp(config); 43 | } 44 | } 45 | 46 | function runReactApp(config: RunAppConfig) { 47 | let element; 48 | const routes = config.router?.config ?? config.routes; 49 | if (routes) { 50 | // react router app 51 | const routerType = config.router?.type ?? 'browser'; 52 | const routerConfig = { 53 | basename: config.router?.basename || '/', 54 | }; 55 | const history = 56 | routerType === 'hash' ? createHashHistory(routerConfig) : createBrowserHistory(routerConfig); 57 | globalTango.history = history; 58 | element = ; 59 | } else { 60 | // single entry app 61 | const SingleEntry = config.singleEntry || 'div'; 62 | element = ; 63 | } 64 | // eslint-disable-next-line react/no-deprecated 65 | ReactDOM.render( 66 | {element}, 67 | config.boot.mountElement, 68 | ); 69 | } 70 | 71 | function runQiankunApp(config: RunAppConfig) { 72 | // FIXME: 需要支持从外层传入 mountId 73 | const mountId = '#root'; 74 | return { 75 | bootstrap() { 76 | return Promise.resolve({}); 77 | }, 78 | 79 | mount() { 80 | runReactApp(config); 81 | }, 82 | 83 | unmount(props: any) { 84 | const target = props.container ?? document; 85 | // eslint-disable-next-line react/no-deprecated 86 | ReactDOM.unmountComponentAtNode(target.querySelector(mountId)); 87 | }, 88 | }; 89 | } 90 | 91 | interface AppContainerProps { 92 | providers?: React.ReactElement[]; 93 | children: React.ReactNode; 94 | } 95 | 96 | function AppContainer({ children: childrenProp, providers = [] }: AppContainerProps) { 97 | let children = childrenProp; 98 | if (providers && providers.length) { 99 | children = providers.reduce((prev, provider) => { 100 | if (React.isValidElement(provider)) { 101 | return React.cloneElement(provider, {}, prev); 102 | } 103 | return prev; 104 | }, childrenProp); 105 | } 106 | return <>{children}; 107 | } 108 | 109 | interface ReactRouterAppProps { 110 | history: History; 111 | routes: RouteConfig[]; 112 | } 113 | 114 | function ReactRouterApp({ history, routes = [] }: ReactRouterAppProps) { 115 | if (!routes.find((route) => !route.path)) { 116 | // 如果用户没有定义 404 路由,则自动添加一个 117 | routes.push({ 118 | component: NotFound, 119 | }); 120 | } 121 | return {renderRoutes(routes)}; 122 | } 123 | 124 | function NotFound() { 125 | return
not found
; 126 | } 127 | -------------------------------------------------------------------------------- /packages/boot/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.3.5](https://github.com/music163/tango-boot/compare/@music163/tango-boot@0.3.4...@music163/tango-boot@0.3.5) (2024-05-31) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * init router history & set default to browser history ([#3](https://github.com/music163/tango-boot/issues/3)) ([c3a1f81](https://github.com/music163/tango-boot/commit/c3a1f8129036fb86ad24c360943af6902dc893fe)) 12 | 13 | 14 | 15 | 16 | 17 | ## [0.3.4](https://github.com/music163/tango-boot/compare/@music163/tango-boot@0.3.3...@music163/tango-boot@0.3.4) (2024-05-28) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * override props for custom designer ([583a4e3](https://github.com/music163/tango-boot/commit/583a4e354de7ba3d7b2264f42fce6d0efe02c964)) 23 | * update props override order ([029e6ff](https://github.com/music163/tango-boot/commit/029e6ff324e996fabd4cf97dc49d355bcf2ee515)) 24 | 25 | 26 | 27 | 28 | 29 | ## [0.3.3](https://github.com/music163/tango-boot/compare/@music163/tango-boot@0.3.2...@music163/tango-boot@0.3.3) (2024-05-28) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * pass designer props only in designer mode ([a29a3d6](https://github.com/music163/tango-boot/commit/a29a3d6d282f7b67be0b1b00b630bdb65f260d4c)) 35 | 36 | 37 | 38 | 39 | 40 | ## [0.3.2](https://github.com/music163/tango-boot/compare/@music163/tango-boot@0.3.1...@music163/tango-boot@0.3.2) (2024-05-15) 41 | 42 | 43 | ### Bug Fixes 44 | 45 | * add navigateTo ([f819074](https://github.com/music163/tango-boot/commit/f8190744c856b182e3ca1179ba6e373c54c90b7e)) 46 | 47 | 48 | 49 | 50 | 51 | ## [0.3.1](https://github.com/music163/tango-boot/compare/@music163/tango-boot@0.3.0...@music163/tango-boot@0.3.1) (2024-04-18) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * add tango.pageStore ([8492613](https://github.com/music163/tango-boot/commit/8492613aecd998cf9fb597f23b0980bde125263d)) 57 | 58 | 59 | 60 | 61 | 62 | # [0.3.0](https://github.com/music163/tango-boot/compare/@music163/tango-boot@0.2.5...@music163/tango-boot@0.3.0) (2024-04-02) 63 | 64 | 65 | ### Bug Fixes 66 | 67 | * check if in sandbox ([60d2e6e](https://github.com/music163/tango-boot/commit/60d2e6ea413915859defbf25169c8433cc296c60)) 68 | 69 | 70 | ### Features 71 | 72 | * refactor defineComponent ([140c38d](https://github.com/music163/tango-boot/commit/140c38d0e961fae35dafb2f9fbfcd3265db61120)) 73 | 74 | 75 | 76 | 77 | 78 | ## [0.2.5](https://github.com/music163/tango-boot/compare/@music163/tango-boot@0.2.4...@music163/tango-boot@0.2.5) (2024-03-07) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * update designerConfig ([4e11e24](https://github.com/music163/tango-boot/commit/4e11e24756550b33f64c80a3d05409ab4db6e5ce)) 84 | 85 | 86 | 87 | 88 | 89 | ## [0.2.4](https://github.com/music163/tango-boot/compare/@music163/tango-boot@0.2.3...@music163/tango-boot@0.2.4) (2024-03-07) 90 | 91 | 92 | ### Bug Fixes 93 | 94 | * custom render in designerConfig ([39b02fb](https://github.com/music163/tango-boot/commit/39b02fb6f680351efef2182fe1a8c26103a7b649)) 95 | 96 | 97 | 98 | 99 | 100 | ## [0.2.3](https://github.com/music163/tango-boot/compare/@music163/tango-boot@0.2.2...@music163/tango-boot@0.2.3) (2024-03-06) 101 | 102 | 103 | ### Bug Fixes 104 | 105 | * update defineComponent ([c4d5e78](https://github.com/music163/tango-boot/commit/c4d5e780f33b9030ae134124baccca3f0381333a)) 106 | 107 | 108 | 109 | 110 | 111 | ## [0.2.2](https://github.com/music163/tango-boot/compare/@music163/tango-boot@0.2.1...@music163/tango-boot@0.2.2) (2024-03-06) 112 | 113 | **Note:** Version bump only for package @music163/tango-boot 114 | 115 | 116 | 117 | 118 | 119 | ## [0.2.1](https://github.com/music163/tango-boot/compare/@music163/tango-boot@0.2.0...@music163/tango-boot@0.2.1) (2023-09-25) 120 | 121 | **Note:** Version bump only for package @music163/tango-boot 122 | 123 | 124 | 125 | 126 | 127 | # [0.2.0](https://github.com/music163/tango-boot/compare/@music163/tango-boot@0.1.4...@music163/tango-boot@0.2.0) (2023-09-19) 128 | 129 | 130 | ### Features 131 | 132 | * defineServices with namespace ([2f83a5b](https://github.com/music163/tango-boot/commit/2f83a5bc8223bcf6d723f758089d6ea5a59a2c8c)) 133 | 134 | 135 | 136 | 137 | 138 | ## [0.1.4](https://github.com/music163/tango-boot/compare/@music163/tango-boot@0.1.3...@music163/tango-boot@0.1.4) (2023-09-05) 139 | 140 | 141 | ### Bug Fixes 142 | 143 | * add singleEntry ([836d339](https://github.com/music163/tango-boot/commit/836d3398689a1396cf392ade20a6347cc16580d3)) 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TangoBoot 2 | 3 | TangoBoot is a frontend framework that serves the NetEase Tango Low-Code applications. It provides standard data requests, state management, and routing solutions, as well as generic runtime utility functions, allowing developers to generate Single Page Applications through less codes. 4 | 5 | Documentation: 6 | 7 | ## How to develop 8 | 9 | Environment requirements: 10 | 11 | - Node >= 16.0.0 12 | - Yarn == 1.x stable 13 | 14 | Run the example app via the following commands: 15 | 16 | ```bash 17 | # install dependencies 18 | yarn 19 | 20 | # build the library 21 | yarn build 22 | 23 | # start the example app 24 | yarn start 25 | ``` 26 | 27 | ## Application Architecture 28 | 29 | The application architecture of TangoBoot uses the View-Model-Service three-layer model. The model layer defines Observable States, the view layer observes the changes of the model and updates automatically, and the service layer is used to create a set of service functions for the consumption of the view layer and the model layer. The diagram is shown in the figure below: 30 | 31 | image 36 | 37 | ## Core API 38 | 39 | - `runApp` create the app entry 40 | - `definePage` define reactive views 41 | - `defineStore` define observable states 42 | - `defineServices` define async service functions 43 | 44 | ## How to use 45 | 46 | ### Create app entry 47 | 48 | The `index.js` is the app entry file,a simple example is: 49 | 50 | ```jsx 51 | import { runApp } from '@music163/tango-boot'; 52 | import routes from './routes'; 53 | import services from './services'; 54 | import home from './stores/home'; 55 | import counter from './stores/counter'; 56 | import './global.less'; 57 | 58 | const { mount, unmount, bootstrap } = runApp({ 59 | boot: { 60 | mountElement: document.querySelector('#root'), 61 | qiankun: false, 62 | }, 63 | 64 | stores: { 65 | home, 66 | counter, 67 | }, 68 | 69 | services, 70 | 71 | router: { 72 | type: 'browser', 73 | config: routes, 74 | }, 75 | }); 76 | 77 | export { mount, unmount, bootstrap }; 78 | ``` 79 | 80 | ### Create Observable States 81 | 82 | Defining a view model through defineStore is very simple, simply declare the state and actions. 83 | 84 | ```jsx 85 | import { defineStore } from '@music163/tango-boot'; 86 | 87 | const counter = defineStore({ 88 | num: 0, 89 | 90 | get() {}, 91 | 92 | decrement: function () { 93 | counter.num--; 94 | }, 95 | 96 | increment: () => counter.num++, 97 | }); 98 | 99 | export default counter; 100 | ``` 101 | 102 | ### Create Reactive Views 103 | 104 | If the view layer needs to listen for state changes, it only needs to wrap the view component with `definePage`. 105 | 106 | ```jsx 107 | import React from 'react'; 108 | import tango, { definePage } from '@music163/tango-boot'; 109 | 110 | class App extends React.Component { 111 | increment = () => { 112 | tango.stores.counter.increment(); 113 | }; 114 | 115 | render() { 116 | return ( 117 |
118 |

Counter: {tango.stores.counter.num}

119 | 122 |
123 | ); 124 | } 125 | } 126 | 127 | export default definePage(App); 128 | ``` 129 | 130 | ### Create Service Functions 131 | 132 | Use `defineServices` to define your remote apis as service functions. 133 | 134 | ```jsx 135 | import { defineServices } from '@music163/tango-boot'; 136 | 137 | export default defineServices({ 138 | list: { 139 | url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users', 140 | formatter: (res) => { 141 | const { data, message } = res; 142 | return { 143 | code: 200, 144 | list: data, 145 | total: data.length, 146 | message, 147 | }; 148 | }, 149 | }, 150 | add: { 151 | url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users', 152 | method: 'post', 153 | }, 154 | update: { 155 | url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users', 156 | method: 'post', 157 | }, 158 | delete: { 159 | url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users?id=1', 160 | }, 161 | }); 162 | ``` 163 | 164 | ## License 165 | 166 | This project is licensed under the terms of the [MIT license](./LICENSE). 167 | -------------------------------------------------------------------------------- /packages/boot/src/defineComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useEffect, useRef } from 'react'; 2 | import { view } from '@risingstack/react-easy-state'; 3 | import { 4 | Dict, 5 | SLOT, 6 | callAll, 7 | hoistNonReactStatics, 8 | isFunctionComponent, 9 | isInTangoDesignMode, 10 | } from '@music163/tango-helpers'; 11 | import tangoBoot from './global'; 12 | import { mergeRefs } from './helpers'; 13 | 14 | interface IPageStateHandlers { 15 | getPageState: () => Dict; 16 | setPageState: (stateValue: Dict) => void; 17 | } 18 | 19 | interface RegisterStateConfig { 20 | /** 21 | * 用户自定义的组件状态 22 | * @param props 23 | * @param instance 24 | * @returns 25 | */ 26 | getInitStates?: (handlers: IPageStateHandlers, props: any, instance: any) => any; 27 | /** 28 | * 用户自定义的触发器行为 29 | * @param handlers 30 | * @returns 31 | */ 32 | getTriggerProps?: (handlers: IPageStateHandlers) => Dict; 33 | } 34 | 35 | interface DesignerRenderProps { 36 | originalProps: Record; 37 | designerProps: Record; 38 | children: React.ReactElement; 39 | } 40 | 41 | interface DesignerConfig { 42 | /** 43 | * 是否可拖拽 44 | */ 45 | draggable?: boolean; 46 | /** 47 | * 是否有包裹容器 48 | */ 49 | hasWrapper?: boolean; 50 | /** 51 | * 容器自定义样式 52 | */ 53 | wrapperStyle?: React.CSSProperties; 54 | /** 55 | * 展示方式 56 | */ 57 | display?: DndBoxProps['display']; 58 | /** 59 | * 自定义渲染 60 | */ 61 | render?: (props: DesignerRenderProps) => React.ReactNode; 62 | /** 63 | * 注入给组件的默认属性 64 | */ 65 | defaultProps?: Record; 66 | } 67 | 68 | interface DefineComponentConfig { 69 | /** 70 | * displayName 71 | */ 72 | name?: string; 73 | /** 74 | * 同步组件状态到 tango.page 上 75 | */ 76 | registerState?: RegisterStateConfig; 77 | /** 78 | * 组件在设计态配置 79 | */ 80 | designerConfig?: DesignerConfig; 81 | } 82 | 83 | interface TangoModelComponentProps extends TangoComponentProps { 84 | /** 85 | * 默认值 86 | */ 87 | defaultValue?: any; 88 | /** 89 | * 内部 ref 90 | */ 91 | innerRef?: React.ComponentRef; 92 | } 93 | 94 | export interface TangoComponentProps { 95 | /** 96 | * 组件 ID (兼容旧版设计) 97 | */ 98 | id?: string; 99 | /** 100 | * 组件 ID,同时用于页面内的状态访问路径 101 | */ 102 | tid?: string; 103 | } 104 | 105 | const registerEmpty = () => ({}); 106 | 107 | // TODO:支持本地组件的属性配置设置 108 | export function defineComponent

( 109 | BaseComponent: React.ComponentType

, 110 | options?: DefineComponentConfig, 111 | ) { 112 | const displayName = 113 | options?.name || BaseComponent.displayName || BaseComponent.name || 'TangoComponent'; 114 | const designerConfig = options?.designerConfig || {}; 115 | 116 | const isFC = isFunctionComponent(BaseComponent); 117 | const isDesignMode = isInTangoDesignMode(); 118 | 119 | // 这里包上 view ,能够响应 model 变化 120 | const InnerModelComponent = view((props: P & TangoModelComponentProps) => { 121 | const ref = useRef(); 122 | 123 | const stateConfig = options?.registerState || {}; 124 | 125 | const getPageStates = stateConfig.getInitStates || registerEmpty; 126 | const { tid, innerRef, ...rest } = props; 127 | 128 | const setPageState = (nextState: Dict) => { 129 | tangoBoot.setPageState(tid, nextState); 130 | }; 131 | 132 | const getPageState = () => { 133 | return tangoBoot.getPageState(tid); 134 | }; 135 | 136 | useEffect(() => { 137 | if (tid) { 138 | const customStates = getPageStates({ getPageState, setPageState }, props, ref.current); 139 | tangoBoot.setPageState(tid, { 140 | ...customStates, 141 | }); 142 | } 143 | return () => { 144 | if (tid) { 145 | tangoBoot.clearPageState(tid); 146 | } 147 | }; 148 | }, [tid]); 149 | 150 | const override: Dict = {}; 151 | 152 | let userTriggerProps = {}; 153 | if (tid) { 154 | userTriggerProps = stateConfig.getTriggerProps?.({ getPageState, setPageState }); 155 | const handlerKeys = Object.keys(userTriggerProps); 156 | if (handlerKeys.length) { 157 | handlerKeys.forEach((key) => { 158 | // FIXME: 应该只需要合并 function 类型的属性,其他属性不需要合并 159 | if (props[key] || override[key]) { 160 | userTriggerProps[key] = callAll(userTriggerProps[key], override[key], props[key]); 161 | } 162 | }); 163 | } 164 | } 165 | 166 | return ( 167 | 173 | ); 174 | }); 175 | 176 | // TIP: view 不支持 forwardRef,这里包一层,包到内部组件去消费,外层支持访问到原始的 ref,避免与原始代码产生冲突 177 | const TangoComponent = forwardRef((props, ref) => { 178 | const { tid } = props; 179 | const refs = isFC ? undefined : ref; 180 | 181 | let renderComponent: (defaultProps?: P) => React.ReactElement; 182 | if (options?.registerState && tid) { 183 | renderComponent = (defaultProps: P) => 184 | React.createElement(InnerModelComponent, { innerRef: refs, ...defaultProps, ...props }); 185 | } else { 186 | renderComponent = (defaultProps: P) => 187 | React.createElement(BaseComponent, { ref: refs, ...defaultProps, ...props }); 188 | } 189 | 190 | if (isDesignMode) { 191 | // design mode 192 | const overrideProps = designerConfig.defaultProps; 193 | const ret = renderComponent(overrideProps as P); 194 | 195 | const designerProps = { 196 | draggable: designerConfig.draggable ?? true, 197 | [SLOT.id]: tid, 198 | [SLOT.dnd]: props[SLOT.dnd], 199 | }; 200 | 201 | if (designerConfig.render) { 202 | // 自定义渲染设计器样式 203 | return designerConfig.render({ designerProps, originalProps: props, children: ret }); 204 | } 205 | 206 | if (designerConfig.hasWrapper) { 207 | return ( 208 | 214 | {ret} 215 | 216 | ); 217 | } else { 218 | return renderComponent({ 219 | ...overrideProps, 220 | ...designerProps, 221 | } as any); 222 | } 223 | } else { 224 | // normal mode 225 | return renderComponent(); 226 | } 227 | }); 228 | 229 | hoistNonReactStatics(TangoComponent, BaseComponent); 230 | TangoComponent.displayName = `defineComponent(${displayName})`; 231 | 232 | return TangoComponent; 233 | } 234 | 235 | interface DndBoxProps extends React.ComponentPropsWithoutRef<'div'> { 236 | name?: string; 237 | display?: 'block' | 'inline-block' | 'inline'; 238 | } 239 | 240 | function DndBox({ name, display, children, style: styleProp, ...rest }: DndBoxProps) { 241 | const style = { 242 | display, 243 | minHeight: 4, 244 | ...styleProp, 245 | }; 246 | return ( 247 |

248 | {children} 249 |
250 | ); 251 | } 252 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | /** @type {import('jest').Config} */ 7 | const config = { 8 | // All imported modules in your tests should be mocked automatically 9 | // automock: false, 10 | 11 | // Stop running tests after `n` failures 12 | // bail: 0, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/q_/7k4h88k50pn0p6423z7smp9r0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls, instances, contexts and results before every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | collectCoverage: true, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: undefined, 25 | 26 | // The directory where Jest should output its coverage files 27 | coverageDirectory: 'coverage', 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // Indicates which provider should be used to instrument code for coverage 35 | // coverageProvider: "babel", 36 | 37 | // A list of reporter names that Jest uses when writing coverage reports 38 | // coverageReporters: [ 39 | // "json", 40 | // "text", 41 | // "lcov", 42 | // "clover" 43 | // ], 44 | 45 | // An object that configures minimum threshold enforcement for coverage results 46 | // coverageThreshold: undefined, 47 | 48 | // A path to a custom dependency extractor 49 | // dependencyExtractor: undefined, 50 | 51 | // Make calling deprecated APIs throw helpful error messages 52 | // errorOnDeprecated: false, 53 | 54 | // The default configuration for fake timers 55 | // fakeTimers: { 56 | // "enableGlobally": false 57 | // }, 58 | 59 | // Force coverage collection from ignored files using an array of glob patterns 60 | // forceCoverageMatch: [], 61 | 62 | // A path to a module which exports an async function that is triggered once before all test suites 63 | // globalSetup: undefined, 64 | 65 | // A path to a module which exports an async function that is triggered once after all test suites 66 | // globalTeardown: undefined, 67 | 68 | // A set of global variables that need to be available in all test environments 69 | // globals: {}, 70 | 71 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 72 | // maxWorkers: "50%", 73 | 74 | // An array of directory names to be searched recursively up from the requiring module's location 75 | // moduleDirectories: [ 76 | // "node_modules" 77 | // ], 78 | 79 | // An array of file extensions your modules use 80 | // moduleFileExtensions: [ 81 | // "js", 82 | // "mjs", 83 | // "cjs", 84 | // "jsx", 85 | // "ts", 86 | // "tsx", 87 | // "json", 88 | // "node" 89 | // ], 90 | 91 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 92 | // moduleNameMapper: {}, 93 | 94 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 95 | // modulePathIgnorePatterns: [], 96 | 97 | // Activates notifications for test results 98 | // notify: false, 99 | 100 | // An enum that specifies notification mode. Requires { notify: true } 101 | // notifyMode: "failure-change", 102 | 103 | // A preset that is used as a base for Jest's configuration 104 | // preset: undefined, 105 | 106 | // Run tests from one or more projects 107 | // projects: undefined, 108 | 109 | // Use this configuration option to add custom reporters to Jest 110 | // reporters: undefined, 111 | 112 | // Automatically reset mock state before every test 113 | // resetMocks: false, 114 | 115 | // Reset the module registry before running each individual test 116 | // resetModules: false, 117 | 118 | // A path to a custom resolver 119 | // resolver: undefined, 120 | 121 | // Automatically restore mock state and implementation before every test 122 | // restoreMocks: false, 123 | 124 | // The root directory that Jest should scan for tests and modules within 125 | // rootDir: undefined, 126 | 127 | // A list of paths to directories that Jest should use to search for files in 128 | // roots: [ 129 | // "" 130 | // ], 131 | 132 | // Allows you to use a custom runner instead of Jest's default test runner 133 | // runner: "jest-runner", 134 | 135 | // The paths to modules that run some code to configure or set up the testing environment before each test 136 | // setupFiles: [], 137 | 138 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 139 | // setupFilesAfterEnv: [], 140 | 141 | // The number of seconds after which a test is considered as slow and reported as such in the results. 142 | // slowTestThreshold: 5, 143 | 144 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 145 | // snapshotSerializers: [], 146 | 147 | // The test environment that will be used for testing 148 | testEnvironment: 'jsdom', 149 | 150 | // Options that will be passed to the testEnvironment 151 | // testEnvironmentOptions: {}, 152 | 153 | // Adds a location field to test results 154 | // testLocationInResults: false, 155 | 156 | // The glob patterns Jest uses to detect test files 157 | testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'], 158 | 159 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 160 | // testPathIgnorePatterns: [ 161 | // "/node_modules/" 162 | // ], 163 | 164 | // The regexp pattern or array of patterns that Jest uses to detect test files 165 | // testRegex: [], 166 | 167 | // This option allows the use of a custom results processor 168 | // testResultsProcessor: undefined, 169 | 170 | // This option allows use of a custom test runner 171 | // testRunner: "jest-circus/runner", 172 | 173 | // A map from regular expressions to paths to transformers 174 | // transform: undefined, 175 | 176 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 177 | // transformIgnorePatterns: [ 178 | // "/node_modules/", 179 | // "\\.pnp\\.[^\\/]+$" 180 | // ], 181 | 182 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 183 | // unmockedModulePathPatterns: undefined, 184 | 185 | // Indicates whether each individual test should be reported during the run 186 | // verbose: undefined, 187 | 188 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 189 | // watchPathIgnorePatterns: [], 190 | 191 | // Whether to use watchman for file crawling 192 | // watchman: true, 193 | }; 194 | 195 | module.exports = config; 196 | --------------------------------------------------------------------------------