├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── example
├── .umirc.js
├── package.json
└── src
│ ├── _mock.js
│ ├── index.css
│ ├── index.js
│ └── model.js
├── layouts
├── ant-design-pro-user
│ ├── index.js
│ ├── logo.svg
│ └── style.less
├── ant-design-pro
│ ├── HeaderDropdown
│ │ ├── index.jsx
│ │ └── index.less
│ ├── SelectLang
│ │ ├── index.jsx
│ │ └── index.less
│ ├── index.js
│ └── logo.svg
└── blankLayout
│ └── index.js
├── package.json
└── src
├── index.js
├── mock-request.js
└── mock-request.test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | yarn.lock
3 | package-lock.json
4 | /example/dist
5 | /lib
6 | .umi
7 | .umi-production
8 | yarn-error.log
9 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "overrides": [
4 | {
5 | "files": ".prettierrc",
6 | "options": { "parser": "json" }
7 | }
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018-present yutingzhao1991 (yutingzhao1991@sina.com)
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # umi-plugin-block-dev
2 |
3 | [](https://npmjs.org/package/umi-plugin-block-dev)
4 | [](https://npmjs.org/package/umi-plugin-block-dev)
5 |
6 | A umi plugin for develop a umi block with umi
7 |
8 | ## Layout
9 |
10 | ### blankLayout
11 |
12 | 
13 |
14 | ### ant-design-pro
15 |
16 | 
17 |
18 | ### ant-design-pro-user
19 |
20 | 
21 |
22 | ## Usage
23 |
24 | Configure in `.umirc.js`,
25 |
26 | ```js
27 | export default {
28 | plugins: [['umi-plugin-block-dev', options]]
29 | };
30 | ```
31 |
32 | And you can use `create-umi` to create a umi block automatically:
33 |
34 | ```sh
35 | $ yarn create umi --block
36 | ```
37 |
38 | ## options
39 |
40 | ```js
41 | {
42 | layout: 'ant-design-pro', // or ant-design-pro-user
43 | menu: {
44 | name: 'demo',
45 | icon: 'home',
46 | },
47 | mockUmiRequest: true // whether to build mock data . _mock.js
48 | }
49 | ```
50 |
51 | ### env
52 |
53 | - BLOCK_DEV_PATH: custom block preview path
54 | - BLOCK_DEV_MOCK_UMI_REQUEST: package mock data to build result, for build static preview site
55 | - BLOCK_PAGES_LAYOUT: custom block Layout
56 |
57 | ## LICENSE
58 |
59 | MIT
60 |
--------------------------------------------------------------------------------
/example/.umirc.js:
--------------------------------------------------------------------------------
1 | import { join } from 'path';
2 |
3 | export default {
4 | plugins: [
5 | [
6 | join(__dirname, '..', require('../package').main || 'index.js'),
7 | {
8 | layout: 'ant-design-pro',
9 | menu: {
10 | name: 'demo',
11 | icon: 'home',
12 | },
13 | mockUmiRequest: true // whether to build mock data . _mock.js \ _mock.ts
14 | }
15 | ],
16 | [
17 | 'umi-plugin-react',
18 | {
19 | dva: true,
20 | locale: true,
21 | antd: true
22 | }
23 | ]
24 | ]
25 | };
26 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "description": "a example block",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "dev": "umi dev",
8 | "build": "umi build"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "devDependencies": {
13 | "umi": "^2.7.0",
14 | "umi-plugin-react": "^1.8.1"
15 | },
16 | "dependencies": {
17 | "umi-request": "^1.0.7"
18 | }
19 | }
--------------------------------------------------------------------------------
/example/src/_mock.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'GET /api/test': {
3 | text: 'I am a test block.',
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/example/src/index.css:
--------------------------------------------------------------------------------
1 | .normal {
2 | height: 100%;
3 | padding: 80px;
4 | }
5 |
--------------------------------------------------------------------------------
/example/src/index.js:
--------------------------------------------------------------------------------
1 | import styles from './index.css';
2 | import { connect } from 'dva';
3 | import { formatMessage } from 'umi-plugin-react/locale';
4 |
5 | import React from 'react';
6 |
7 | const Welcome = () => (
8 |
9 | Want to add more pages? Please refer to{' '}
10 |
15 | use block
16 |
17 | {formatMessage({ id: '.', defaultMessage: '.' })}
18 |
19 | );
20 |
21 | export default connect(({ test }) => ({
22 | test
23 | }))(Welcome);
24 |
--------------------------------------------------------------------------------
/example/src/model.js:
--------------------------------------------------------------------------------
1 | import request from 'umi-request';
2 |
3 | export default {
4 | namespace: 'test',
5 | state: {
6 | text: 'loading...',
7 | },
8 | effects: {
9 | *fetch(_, { call, put }) {
10 | const response = yield call(request, '/api/test');
11 | yield put({
12 | type: 'save',
13 | payload: response,
14 | });
15 | },
16 | },
17 | reducers: {
18 | save(state, { payload }) {
19 | return {
20 | ...state,
21 | ...payload,
22 | };
23 | },
24 | },
25 | subscriptions: {
26 | setup({ dispatch }) {
27 | dispatch({ type:'fetch' });
28 | },
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/layouts/ant-design-pro-user/index.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import { formatMessage } from 'umi/locale';
3 | import Link from 'umi/link';
4 | import { Icon } from 'antd';
5 | import logo from './logo.svg';
6 | import './style.less';
7 |
8 | const links = [
9 | {
10 | key: 'help',
11 | title: formatMessage({ id: 'layout.user.link.help' }),
12 | href: ''
13 | },
14 | {
15 | key: 'privacy',
16 | title: formatMessage({ id: 'layout.user.link.privacy' }),
17 | href: ''
18 | },
19 | {
20 | key: 'terms',
21 | title: formatMessage({ id: 'layout.user.link.terms' }),
22 | href: ''
23 | }
24 | ];
25 |
26 | const copyright = (
27 |
28 | Copyright 2018 蚂蚁金服体验技术部出品
29 |
30 | );
31 |
32 | class UserLayout extends React.PureComponent {
33 | render() {
34 | const { children } = this.props;
35 | return (
36 |
37 |
38 |
39 |
40 |
41 |

42 |
Ant Design
43 |
44 |
45 |
46 | Ant Design 是西湖区最具影响力的 Web 设计规范
47 |
48 |
49 | {children}
50 |
51 |
52 | );
53 | }
54 | }
55 |
56 | export default UserLayout;
57 |
--------------------------------------------------------------------------------
/layouts/ant-design-pro-user/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
44 |
--------------------------------------------------------------------------------
/layouts/ant-design-pro-user/style.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | :global {
4 | .umi-block-dev {
5 | display: flex;
6 | flex-direction: column;
7 | height: 100vh;
8 | overflow: auto;
9 | background: @layout-body-background;
10 |
11 | .lang {
12 | text-align: right;
13 | width: 100%;
14 | height: 40px;
15 | line-height: 44px;
16 | :global(.ant-dropdown-trigger) {
17 | margin-right: 24px;
18 | }
19 | }
20 |
21 | .content {
22 | padding: 32px 0;
23 | flex: 1;
24 | }
25 |
26 | .top {
27 | text-align: center;
28 | }
29 |
30 | .header {
31 | height: 44px;
32 | line-height: 44px;
33 | a {
34 | text-decoration: none;
35 | }
36 | }
37 |
38 | .logo {
39 | height: 44px;
40 | vertical-align: top;
41 | margin-right: 16px;
42 | }
43 |
44 | .title {
45 | font-size: 33px;
46 | color: @heading-color;
47 | font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
48 | font-weight: 600;
49 | position: relative;
50 | top: 2px;
51 | }
52 |
53 | .desc {
54 | font-size: @font-size-base;
55 | color: @text-color-secondary;
56 | margin-top: 12px;
57 | margin-bottom: 40px;
58 | }
59 | }
60 |
61 | @media (min-width: @screen-md-min) {
62 | .umi-block-dev {
63 | background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
64 | background-repeat: no-repeat;
65 | background-position: center 110px;
66 | background-size: 100%;
67 |
68 | .content {
69 | padding: 32px 0 24px 0;
70 | }
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/layouts/ant-design-pro/HeaderDropdown/index.jsx:
--------------------------------------------------------------------------------
1 | import { Dropdown } from 'antd';
2 | import React from 'react';
3 | import classNames from 'classnames';
4 | import styles from './index.less';
5 |
6 | const HeaderDropdown = ({ overlayClassName: cls, ...restProps }) => (
7 |
11 | );
12 |
13 | export default HeaderDropdown;
14 |
--------------------------------------------------------------------------------
/layouts/ant-design-pro/HeaderDropdown/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/es/style/themes/default.less';
2 |
3 | .container > * {
4 | background-color: #fff;
5 | border-radius: 4px;
6 | box-shadow: @shadow-1-down;
7 | }
8 |
9 | @media screen and (max-width: @screen-xs) {
10 | .container {
11 | width: 100% !important;
12 | }
13 | .container > * {
14 | border-radius: 0 !important;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/layouts/ant-design-pro/SelectLang/index.jsx:
--------------------------------------------------------------------------------
1 | import { Icon, Menu } from 'antd';
2 | import { getLocale, setLocale } from 'umi-plugin-react/locale';
3 | import React from 'react';
4 | import classNames from 'classnames';
5 | import HeaderDropdown from '../HeaderDropdown';
6 | import styles from './index.less';
7 |
8 | const SelectLang = props => {
9 | const { className } = props;
10 | const selectedLang = getLocale();
11 | const changeLang = ({ key }) => setLocale(key);
12 | const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR'];
13 | const languageLabels = {
14 | 'zh-CN': '简体中文',
15 | 'zh-TW': '繁体中文',
16 | 'en-US': 'English',
17 | 'pt-BR': 'Português'
18 | };
19 | const languageIcons = {
20 | 'zh-CN': '🇨🇳',
21 | 'zh-TW': '🇭🇰',
22 | 'en-US': '🇬🇧',
23 | 'pt-BR': '🇧🇷'
24 | };
25 | const langMenu = (
26 |
40 | );
41 | return (
42 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | );
55 | };
56 |
57 | export default SelectLang;
58 |
--------------------------------------------------------------------------------
/layouts/ant-design-pro/SelectLang/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/es/style/themes/default.less';
2 |
3 | .menu {
4 | :global(.anticon) {
5 | margin-right: 8px;
6 | }
7 | :global(.ant-dropdown-menu-item) {
8 | min-width: 160px;
9 | }
10 | }
11 |
12 | .dropDown {
13 | line-height: @layout-header-height;
14 | vertical-align: top;
15 | cursor: pointer;
16 | > i {
17 | font-size: 16px !important;
18 | transform: none !important;
19 | svg {
20 | position: relative;
21 | top: -1px;
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/layouts/ant-design-pro/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import logo from './logo.svg';
3 | import { BasicLayout, SettingDrawer } from '@ant-design/pro-layout';
4 | import SelectLang from './SelectLang';
5 |
6 | export default props => {
7 | const { children } = props;
8 | const [settings, changeSetting] = useState({});
9 | return (
10 |
11 | }
16 | >
17 | {children}
18 |
19 | changeSetting(settings)}
22 | />
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/layouts/ant-design-pro/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/layouts/blankLayout/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BasicLayout } from '@ant-design/pro-layout';
3 |
4 | const Layout = ({ children }) => (
5 |
6 | {children}
7 |
8 | );
9 |
10 | export default Layout;
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "umi-plugin-block-dev",
3 | "version": "3.0.3",
4 | "description": "A umi plugin for develop a umi block with umi",
5 | "license": "MIT",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/umijs/umi-plugin-block-dev"
9 | },
10 | "homepage": "https://github.com/umijs/umi-plugin-block-dev",
11 | "authors": [
12 | "yutingzhao1991 ",
13 | "chenshuai2144 "
14 | ],
15 | "bugs": {
16 | "url": "https://github.com/umijs/umi-plugin-block-dev/issues"
17 | },
18 | "peerDependencies": {
19 | "@ant-design/pro-layout": "^4.11.0-1",
20 | "umi": "2.x"
21 | },
22 | "main": "lib/index.js",
23 | "scripts": {
24 | "build": "umi-tools build",
25 | "prepublishOnly": "umi-tools build",
26 | "pub": "npm run build && npm publish",
27 | "test": "umi-test"
28 | },
29 | "devDependencies": {
30 | "umi-plugin-react": "^1.9.7",
31 | "umi-request": "^1.0.8",
32 | "umi-test": "^1.2.2",
33 | "umi-tools": "*"
34 | },
35 | "files": [
36 | "lib",
37 | "src",
38 | "layouts"
39 | ],
40 | "dependencies": {
41 | "path-to-regexp": "^2.4.0",
42 | "umi": "^2.12.9",
43 | "uppercamelcase": "^3.0.0"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // ref:
2 | // - https://umijs.org/plugin/develop.html
3 | import { join, dirname } from 'path';
4 | import { existsSync, readdirSync } from 'fs';
5 | import assert from 'assert';
6 | import upperCamelCase from 'uppercamelcase';
7 |
8 | if (!process.env.PAGES_PATH) {
9 | process.env.PAGES_PATH = 'src';
10 | }
11 |
12 | const layouts = ['ant-design-pro', 'ant-design-pro-user', 'blankLayout'];
13 |
14 | function findGitDir(thePath) {
15 | if (thePath === '/') {
16 | return null;
17 | }
18 | const items = readdirSync(thePath);
19 | if (items.includes('.git')) {
20 | return thePath;
21 | } else {
22 | return findGitDir(dirname(thePath));
23 | }
24 | }
25 |
26 | export function getNameFromPkg(pkg) {
27 | if (!pkg.name) {
28 | return null;
29 | }
30 | return pkg.name.split('/').pop();
31 | }
32 |
33 | export default function(api, options = {}) {
34 | api.registerCommand('block_dev', {}, ({ _ }) => {
35 | process.env.PAGES_PATH = `${_[0]}/src`;
36 | process.env.UMI_UI = 'none';
37 | require(require.resolve(`umi/lib/scripts/dev`));
38 | });
39 |
40 | const { paths, debug } = api;
41 | const path = process.env.BLOCK_DEV_PATH || options.path || '/';
42 | const blockConfig = require(join(paths.cwd, 'package.json')).blockConfig;
43 |
44 | let subBlocks = [];
45 |
46 | // 支持区块依赖
47 | if (blockConfig && blockConfig.dependencies) {
48 | debug('find dependencies in package.json');
49 | const gitRoot = findGitDir(paths.cwd);
50 | debug(`get gitRoot: ${gitRoot}`);
51 | if (gitRoot) {
52 | subBlocks = blockConfig.dependencies.map(d => {
53 | const subBlockPath = join(gitRoot, d);
54 | const subBlockConfig = require(join(subBlockPath, 'package.json'));
55 | const subBlockName = upperCamelCase(getNameFromPkg(subBlockConfig));
56 | return {
57 | name: subBlockName,
58 | path: subBlockPath
59 | };
60 | });
61 | } else {
62 | throw new Error('Not find git root, can not use dependencies.');
63 | }
64 | }
65 |
66 | // 是否 mock 数据,用于测试
67 | const mockUmiRequest =
68 | process.env.BLOCK_DEV_MOCK_UMI_REQUEST === 'true' ||
69 | options.mockUmiRequest ||
70 | false;
71 |
72 | api.modifyDefaultConfig(memo => {
73 | // 这个环境变量是为了截图的时候可以动态设置 layout
74 | // 所以会优先从 环境变量里面取
75 | const layoutConfig = process.env.BLOCK_PAGES_LAYOUT || options.layout;
76 |
77 | if (layoutConfig) {
78 | assert(
79 | layouts.includes(layoutConfig),
80 | `layout must be one of ${layouts.join(',')}`
81 | );
82 | const layout = join(__dirname, `../layouts/${layoutConfig}`);
83 | const pathToLayout = (paths.absPagesPath, layout);
84 | return {
85 | ...memo,
86 | routes: [
87 | {
88 | path: '/',
89 | component: pathToLayout,
90 | routes: [
91 | {
92 | path,
93 | ...options.menu,
94 | component: './',
95 | exact: false
96 | }
97 | ]
98 | }
99 | ],
100 | extraBabelIncludes: [layout].concat(subBlocks.map(b => b.path))
101 | };
102 | }
103 | return {
104 | ...memo,
105 | routes: [
106 | {
107 | ...options.menu,
108 | path,
109 | component: './',
110 | exact: false
111 | }
112 | ]
113 | };
114 | });
115 |
116 | if (mockUmiRequest && existsSync(join(paths.absPagesPath, '_mock.js'))) {
117 | // build mock data to dist, for static block demo
118 | api.addEntryImportAhead({
119 | source: join(paths.absPagesPath, '_mock.js'),
120 | specifier: '__block_mock'
121 | });
122 | api.addEntryCodeAhead(`
123 | window.g_block_mock = __block_mock;
124 | `);
125 | api.chainWebpackConfig(webpackConfig => {
126 | webpackConfig.resolve.alias.set(
127 | 'umi-request$',
128 | join(__dirname, 'mock-request.js')
129 | );
130 | });
131 | }
132 | api.addHTMLStyle({
133 | content: `
134 | body,html,#root{
135 | height:100%
136 | }
137 | `
138 | });
139 | api.chainWebpackConfig(webpackConfig => {
140 | webpackConfig.resolve.alias.set('@', join(paths.absSrcPath, '@'));
141 | subBlocks.forEach(b => {
142 | webpackConfig.resolve.alias.set(`./${b.name}`, join(b.path, 'src'));
143 | });
144 | });
145 | }
146 |
--------------------------------------------------------------------------------
/src/mock-request.js:
--------------------------------------------------------------------------------
1 | import pathToRegexp from 'path-to-regexp';
2 |
3 | function isUrlMatch(define, url) {
4 | return pathToRegexp(define).exec(url);
5 | }
6 |
7 | export default (url, options = {}, mockData) => {
8 | console.log(`mock for url: ${url}`);
9 | const { params, method = 'get' } = options;
10 | mockData = mockData || window.g_block_mock;
11 | const keys = Object.keys(mockData);
12 | let mockInfo = keys.find(item => {
13 | let [aMethod, aUrl] = item.split(/\s+/);
14 | if (!aUrl) {
15 | aUrl = aMethod;
16 | aMethod = '*';
17 | }
18 | if (
19 | (aMethod === '*' ||
20 | aMethod.toLocaleLowerCase() === method.toLocaleLowerCase()) &&
21 | isUrlMatch(aUrl, url)
22 | ) {
23 | return true;
24 | }
25 | return false;
26 | });
27 | console.log(`find mock data key: ${mockInfo}`);
28 | if (mockInfo) {
29 | mockInfo = mockData[mockInfo];
30 | let retData;
31 | if (typeof mockInfo === 'function') {
32 | retData = mockInfo(
33 | {
34 | query: params,
35 | params: {} // TODO
36 | },
37 | {}
38 | );
39 | } else {
40 | retData = mockInfo;
41 | }
42 | return Promise.resolve(retData);
43 | } else {
44 | throw new Error('not find mock data');
45 | }
46 | };
47 |
--------------------------------------------------------------------------------
/src/mock-request.test.js:
--------------------------------------------------------------------------------
1 | import request from './mock-request';
2 |
3 | describe('mock-request', () => {
4 | it('mock data right', async () => {
5 | const ret = await request(
6 | '/api/test',
7 | {},
8 | {
9 | 'GET /api/test': 'testres'
10 | }
11 | );
12 | expect(ret).toEqual('testres');
13 |
14 | const ret2 = await request(
15 | '/api/test',
16 | {},
17 | {
18 | 'GET /api/:id': 'testres2'
19 | }
20 | );
21 | expect(ret2).toEqual('testres2');
22 |
23 | const ret3 = await request(
24 | '/api/test',
25 | {
26 | method: 'post'
27 | },
28 | {
29 | 'POST /api/test': () => {
30 | return 'testres-post';
31 | },
32 | 'GET /api/test': () => {
33 | return 'testres3';
34 | }
35 | }
36 | );
37 | expect(ret3).toEqual('testres-post');
38 |
39 | const ret4 = await request(
40 | '/api/test',
41 | {},
42 | {
43 | '/api/test': 'testres4'
44 | }
45 | );
46 | expect(ret4).toEqual('testres4');
47 | });
48 | });
49 |
--------------------------------------------------------------------------------