├── example ├── package.json ├── .umirc.js ├── models │ └── counter.ts ├── pages │ └── index.tsx ├── app.ts └── tsconfig.json ├── .fatherrc.ts ├── .prettierrc ├── .gitignore ├── src ├── utils │ ├── getExportContent.tsx │ ├── getProviderContent.tsx │ └── getModelContent.tsx ├── runtime.ts ├── constants.ts ├── index.test.js └── index.ts ├── jest.config.js ├── tsconfig.json ├── .github └── workflows │ └── ci.yml ├── package.json ├── circle.yml ├── README_CN.md └── README.md /example/package.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.fatherrc.ts: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | cjs: 'babel', 4 | disableTypeCheck: true, 5 | }; 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 90 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /yarn.lock 3 | /package-lock.json 4 | /example/dist 5 | /test/fixtures/*/.umi 6 | /lib 7 | /coverage 8 | .umi 9 | .umi-production 10 | node_modules 11 | -------------------------------------------------------------------------------- /example/.umirc.js: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | export default { 4 | plugins: [ 5 | [join(__dirname, '..', require('../package').main || 'index.js')], 6 | ['@umijs/plugin-model'], 7 | ], 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/getExportContent.tsx: -------------------------------------------------------------------------------- 1 | export default ( 2 | modelPath: string, 3 | ) => `import { InitialState as InitialStateType } from '../${modelPath}'; 4 | 5 | export type InitialState = InitialStateType; 6 | `; 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | collectCoverageFrom: [ 4 | `src/**/*.{js,jsx,ts,tsx}`, 5 | '!**/fixtures/**', 6 | ], 7 | coveragePathIgnorePatterns: [ 8 | '/test', 9 | ], 10 | } 11 | -------------------------------------------------------------------------------- /src/runtime.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DIR_NAME } from './constants'; 3 | 4 | export function rootContainer(container: React.ReactNode) { 5 | return React.createElement( 6 | require(`@tmp/${DIR_NAME}/Provider`).default, 7 | null, 8 | container, 9 | ); 10 | } -------------------------------------------------------------------------------- /example/models/counter.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export default () => { 4 | const [ counter, setCounter ] = useState(0); 5 | const increment = () => setCounter(c => c + 1); 6 | const decrement = () => setCounter(c => c - 1); 7 | return { counter, increment, decrement }; 8 | } 9 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | export const DIR_NAME = 'plugin-init'; 4 | export const MODEL_NAME = 'initialState'; 5 | export const RELATIVE_MODEL = join(DIR_NAME, 'models', MODEL_NAME); 6 | export const RELATIVE_MODEL_PATH = `${RELATIVE_MODEL}.ts`; 7 | export const RELATIVE_EXPORT = join(DIR_NAME, 'exports'); 8 | export const RELATIVE_EXPORT_PATH = `${RELATIVE_EXPORT}.ts`; 9 | -------------------------------------------------------------------------------- /example/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useModel } from 'umi'; 3 | 4 | export default () => { 5 | const { initialState = {}, loading, refresh } = useModel('@@initialState'); 6 | return (<> 7 |
8 | { 9 | loading ? 'loading' : <>name: {initialState.name} 10 | } 11 |
12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /example/app.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const getInitialState = async() => { 4 | const mockService = () => new Promise<{ email: string, name: string }>(res => setTimeout(()=>{ 5 | res({ 6 | name: 'troy', 7 | email: 'troy.lty@alipay.com', 8 | }) 9 | }, 2000)); 10 | const userInfo = await mockService(); 11 | 12 | return { 13 | avatar: `avatarUrl`, 14 | gender: 'male', 15 | ...userInfo, 16 | }; 17 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/dist", 4 | "module": "esnext", 5 | "target": "esnext", 6 | "lib": ["esnext", "dom"], 7 | "sourceMap": true, 8 | "baseUrl": ".", 9 | "jsx": "react", 10 | "allowSyntheticDefaultImports": true, 11 | "moduleResolution": "node", 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitReturns": true, 14 | "suppressImplicitAnyIndexErrors": true, 15 | "noUnusedLocals": true, 16 | "allowJs": true, 17 | "experimentalDecorators": true, 18 | "strict": true 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | "build", 23 | "dist", 24 | "script" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node_version: [8.x, 10.x, 12.x] 11 | os: [ubuntu-latest] 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: Use Node.js ${{ matrix.node_version }} 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: ${{ matrix.node_version }} 18 | - run: yarn 19 | - run: yarn build 20 | - run: yarn test --forceExit 21 | env: 22 | CI: true 23 | HEADLESS: false 24 | PROGRESS: none 25 | NODE_ENV: test 26 | NODE_OPTIONS: --max_old_space_size=4096 27 | 28 | -------------------------------------------------------------------------------- /src/utils/getProviderContent.tsx: -------------------------------------------------------------------------------- 1 | export default ` 2 | import React, { useRef, useEffect } from 'react'; 3 | import { useModel } from 'umi'; 4 | if (typeof useModel !== 'function') { 5 | throw new Error('[plugin-initial-state]: useModel is not a function, @umijs/plugin-model is required.') 6 | } 7 | 8 | interface Props { 9 | children: React.ReactNode; 10 | } 11 | export default (props: Props) => { 12 | const { children } = props; 13 | const appLoaded = useRef(false); 14 | const { loading = false } = useModel('@@initialState') || {}; 15 | useEffect(()=>{ 16 | if(!loading){ 17 | appLoaded.current = true 18 | } 19 | }, [loading]) 20 | // initial state loading 时,阻塞渲染 21 | if (loading && !appLoaded.current) { 22 | return null; 23 | } 24 | return children; 25 | }; 26 | ` -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/dist", 4 | "module": "esnext", 5 | "target": "esnext", 6 | "lib": ["esnext", "dom"], 7 | "sourceMap": true, 8 | "baseUrl": ".", 9 | "jsx": "react", 10 | "allowSyntheticDefaultImports": true, 11 | "moduleResolution": "node", 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitReturns": true, 14 | "suppressImplicitAnyIndexErrors": true, 15 | "noUnusedLocals": true, 16 | "allowJs": true, 17 | "experimentalDecorators": true, 18 | "strict": true, 19 | "paths": { 20 | "@/*": ["./*"], 21 | "@tmp/*": ["./pages/.umi/*"] 22 | } 23 | }, 24 | "exclude": [ 25 | "node_modules", 26 | "build", 27 | "dist", 28 | "script" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import pluginFunc from './index'; 3 | 4 | const examplePath = join(__dirname, '../example'); 5 | 6 | describe('plugin-request', () => { 7 | const getMockAPI = (writeTmpFile = () => {}) => { 8 | return { 9 | addRuntimePluginKey() {}, 10 | onGenerateFiles(handler) { 11 | handler(); 12 | }, 13 | addRuntimePlugin() {}, 14 | register() {}, 15 | paths: { 16 | absTmpDirPath: '/test/page/.umi', 17 | absSrcPath: examplePath, 18 | cwd: examplePath, 19 | }, 20 | findJS: p => `${p}.ts`, 21 | winPath() { 22 | return '/winpathtest'; 23 | }, 24 | addUmiExports() {}, 25 | writeTmpFile, 26 | }; 27 | }; 28 | 29 | test('normal', () => { 30 | const writeTmpFile = jest.fn(); 31 | pluginFunc(getMockAPI(writeTmpFile)); 32 | 33 | expect(writeTmpFile).toHaveBeenCalledTimes(3); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@umijs/plugin-initial-state", 3 | "version": "1.0.1", 4 | "description": "Umi plugin to store initial state globally.", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/umijs/plugin-initial-state" 9 | }, 10 | "homepage": "https://github.com/umijs/plugin-initial-state", 11 | "authors": [ 12 | "Troy Li " 13 | ], 14 | "bugs": { 15 | "url": "https://github.com/umijs/plugin-initial-state/issues" 16 | }, 17 | "peerDependencies": { 18 | "@umijs/plugin-model": "^0.1.0", 19 | "react": "^16.0.0", 20 | "umi": "2.x" 21 | }, 22 | "main": "lib/index.js", 23 | "scripts": { 24 | "build": "father-build", 25 | "test": "umi-test --coverage", 26 | "debug": "umi-test", 27 | "prepublishOnly": "npm run build && np --no-cleanup --yolo --no-publish" 28 | }, 29 | "devDependencies": { 30 | "@testing-library/jest-dom": "^4.2.4", 31 | "@testing-library/react": "^9.3.2", 32 | "@types/jest": "^24.0.23", 33 | "@types/node": "^12.12.11", 34 | "father-build": "^1.15.0", 35 | "np": "^5.2.1", 36 | "umi-test": "^1.3.3", 37 | "umi-types": "^0.5.7" 38 | }, 39 | "files": [ 40 | "lib", 41 | "src" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2.0 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:latest-browsers 11 | environment: 12 | CI: true 13 | NODE_ENV: test 14 | NODE_OPTIONS: --max_old_space_size=4096 15 | NPM_CONFIG_LOGLEVEL: error 16 | JOBS: max # https://gist.github.com/ralphtheninja/f7c45bdee00784b41fed 17 | branches: 18 | ignore: 19 | - gh-pages # list of branches to ignore 20 | - /release\/.*/ # or ignore regexes 21 | working_directory: ~/father 22 | 23 | steps: 24 | - checkout 25 | - restore_cache: 26 | key: node-modules-{{ checksum "package.json" }} 27 | - run: sudo npm install -g cnpm 28 | - run: cnpm install --registry=https://registry.npmjs.org 29 | - run: cnpm run build 30 | - run: 31 | command: npm run test -- --forceExit --detectOpenHandles --runInBand --maxWorkers=2 32 | no_output_timeout: 300m 33 | - run: bash <(curl -s https://codecov.io/bash) 34 | - save_cache: 35 | key: node-modules-{{ checksum "package.json" }} 36 | paths: 37 | - ./node_modules 38 | -------------------------------------------------------------------------------- /src/utils/getModelContent.tsx: -------------------------------------------------------------------------------- 1 | export default (relEntryFile: string) => `import { useState, useEffect } from 'react'; 2 | import { Models } from 'umi'; 3 | import * as app from '@/app'; 4 | 5 | export type InitialState = Models<'@@initialState'>; 6 | async function getInitialState() { 7 | if (!app.getInitialState) { 8 | throw new Error('getInitialState is not defined in ${relEntryFile}'); 9 | } 10 | return await app.getInitialState(); 11 | } 12 | 13 | type ThenArg = T extends Promise ? U : T 14 | 15 | const initState = { 16 | initialState: undefined as ThenArg> | undefined, 17 | loading: true, 18 | error: undefined as Error | undefined, 19 | } 20 | 21 | export default () => { 22 | const [ state, setState ] = useState(initState); 23 | 24 | const refresh = async() => { 25 | setState(s => ({ ...s, loading: true, initialState: undefined, error: undefined })) 26 | try { 27 | const asyncFunc = () => new Promise>(res => res(getInitialState())); 28 | const ret = await asyncFunc(); 29 | setState(s => ({ ...s, initialState: ret, loading: false })); 30 | } catch(e) { 31 | setState(s => ({ ...s, error: e, loading: false })); 32 | } 33 | }; 34 | 35 | useEffect(()=>{ 36 | refresh(); 37 | }, []); 38 | 39 | return { 40 | ...state, 41 | refresh, 42 | } 43 | } 44 | ` 45 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { IApi } from 'umi-types'; 2 | import { join, relative } from 'path'; 3 | import providerContent from './utils/getProviderContent'; 4 | import getModelContent from './utils/getModelContent'; 5 | import getExportContent from './utils/getExportContent'; 6 | import { 7 | DIR_NAME, 8 | RELATIVE_MODEL, 9 | RELATIVE_MODEL_PATH, 10 | RELATIVE_EXPORT, 11 | RELATIVE_EXPORT_PATH, 12 | } from './constants'; 13 | 14 | export default (api: IApi) => { 15 | const { paths, findJS } = api; 16 | // 注册 getInitialState 方法 17 | api.addRuntimePluginKey('getInitialState'); 18 | 19 | api.writeTmpFile(join(DIR_NAME, 'Provider.tsx'), providerContent); 20 | 21 | // Add provider to prevent render 22 | api.addRuntimePlugin(join(__dirname, './runtime')); 23 | 24 | api.onGenerateFiles(() => { 25 | const entryFile = findJS(join(paths.absSrcPath, 'app')); 26 | if (entryFile) { 27 | const relEntryFile = relative(paths.cwd, entryFile); 28 | api.writeTmpFile(RELATIVE_MODEL_PATH, getModelContent(relEntryFile)); 29 | api.writeTmpFile(RELATIVE_EXPORT_PATH, getExportContent(RELATIVE_MODEL)); 30 | } else { 31 | api.log.warn( 32 | '[@umijs/plugin-initial-state]: 检测到 @umijs/plugin-initial-state 插件已经开启,但是不存在 app.ts/js 入口文件。', 33 | ); 34 | } 35 | }); 36 | 37 | api.addUmiExports([ 38 | { 39 | exportAll: true, 40 | source: api.winPath(join(api.paths.absTmpDirPath, RELATIVE_EXPORT)), 41 | }, 42 | ]); 43 | 44 | api.register('addExtraModels', () => [ 45 | { 46 | absPath: join(paths.absTmpDirPath, RELATIVE_MODEL_PATH), 47 | namespace: '@@initialState', 48 | }, 49 | ]); 50 | }; 51 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | [English](./README.md) | 中文文档 2 | 3 | # @umijs/plugin-initial-state 4 | 5 | [![codecov](https://codecov.io/gh/umijs/plugin-initial-state/branch/master/graph/badge.svg)](https://codecov.io/gh/umijs/plugin-initial-state) 6 | [![NPM version](https://img.shields.io/npm/v/@umijs/plugin-initial-state.svg?style=flat)](https://npmjs.org/package/@umijs/plugin-initial-state) 7 | [![CircleCI](https://circleci.com/gh/umijs/plugin-initial-state/tree/master.svg?style=svg)](https://circleci.com/gh/umijs/plugin-initial-state/tree/master) 8 | [![GitHub Actions status](https://github.com/umijs/plugin-initial-state/workflows/Node%20CI/badge.svg)](https://github.com/umijs/plugin-initial-state) 9 | [![NPM downloads](http://img.shields.io/npm/dm/@umijs/plugin-initial-state.svg?style=flat)](https://npmjs.org/package/@umijs/plugin-initial-state) 10 | 11 | 在全局中注册初始化信息的 umi plugin. 12 | 13 | ## 安装 14 | 15 | ```bash 16 | # or yarn 17 | $ npm install @umijs/plugin-initial-state --save 18 | ``` 19 | 20 | ## 用法 21 | 22 | 3 步开始使用 @umijs/plugin-initial-state 23 | 24 | ### 1. 在 `.umirc.js` 中配置 25 | 26 | ```js 27 | export default { 28 | plugins: [['@umijs/plugin-initial-state', options]], 29 | }; 30 | ``` 31 | 32 | ### 2. 在 `src/app.ts` 中配置 getInitialState 方法 33 | 34 | ```js 35 | export async function getInitialState() { 36 | return 'Hello World'; 37 | } 38 | ``` 39 | 40 | ### 3. 在 React 组件或其他 Model 中使用 initialState 41 | 42 | ```js 43 | import React from 'react'; 44 | import { useModel } from 'umi'; 45 | 46 | export default () => { 47 | const { initialState, loading, refresh } = useModel('@@initialState'); 48 | return <>{ loading ? 'loading...' : initialState }; 49 | }; 50 | ``` 51 | 52 | 完整例子可参考 [./example](https://github.com/umijs/plugin-initial-state/tree/master/example). 53 | 54 | ## LICENSE 55 | 56 | MIT 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | English | [中文文档](./README_CN.md) 2 | 3 | # @umijs/plugin-initial-state 4 | 5 | [![codecov](https://codecov.io/gh/umijs/plugin-initial-state/branch/master/graph/badge.svg)](https://codecov.io/gh/umijs/plugin-initial-state) 6 | [![NPM version](https://img.shields.io/npm/v/@umijs/plugin-initial-state.svg?style=flat)](https://npmjs.org/package/@umijs/plugin-initial-state) 7 | [![CircleCI](https://circleci.com/gh/umijs/plugin-initial-state/tree/master.svg?style=svg)](https://circleci.com/gh/umijs/plugin-initial-state/tree/master) 8 | [![GitHub Actions status](https://github.com/umijs/plugin-initial-state/workflows/Node%20CI/badge.svg)](https://github.com/umijs/plugin-initial-state) 9 | [![NPM downloads](http://img.shields.io/npm/dm/@umijs/plugin-initial-state.svg?style=flat)](https://npmjs.org/package/@umijs/plugin-initial-state) 10 | 11 | Umi plugin to store initial state globally. 12 | 13 | ## Install 14 | 15 | ```bash 16 | # or yarn 17 | $ npm install @umijs/plugin-initial-state --save 18 | ``` 19 | 20 | ## Usage 21 | 22 | Getting started in 3 steps. 23 | 24 | ### 1. Configure in `.umirc.js` 25 | 26 | ```js 27 | export default { 28 | plugins: [['@umijs/plugin-initial-state', options]], 29 | }; 30 | ``` 31 | 32 | ### 2. Add getInitialState into `src/app.ts` 33 | 34 | ```js 35 | export async function getInitialState() { 36 | return 'Hello World'; 37 | } 38 | ``` 39 | 40 | ### 3. Use it in your React Component or other models 41 | 42 | ```js 43 | import React from 'react'; 44 | import { useModel } from 'umi'; 45 | 46 | export default () => { 47 | const { initialState, loading, refresh } = useModel('@@initialState'); 48 | return <>{ loading ? 'loading...' : initialState }; 49 | }; 50 | ``` 51 | 52 | Full example can find in [./example](https://github.com/umijs/plugin-initial-state/tree/master/example). 53 | 54 | ## LICENSE 55 | 56 | MIT 57 | --------------------------------------------------------------------------------