├── demo
├── .gitignore
├── src
│ └── index.tsx
├── tsconfig.json
├── index.html
├── package.json
└── webpack.config.js
├── .travis.yml
├── .npmignore
├── .gitignore
├── .editorconfig
├── lib
├── index.js
└── util.js
├── package.json
├── README.md
└── test
└── index.test.js
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "6"
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | demo
3 | test
4 | .editorconfig
5 | .gitignore
6 | .travis.yml
7 | .idea
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | node_modules/
3 | .idea
4 | package-lock.json
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | charset=utf-8
3 | end_of_line=lf
4 | insert_final_newline=false
5 | indent_style=space
6 | indent_size=2
--------------------------------------------------------------------------------
/demo/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import {Button} from "antd";
3 | import {render} from "react-dom";
4 |
5 | render(, document.getElementById('app'));
--------------------------------------------------------------------------------
/demo/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es5",
5 | "sourceMap": true,
6 | "jsx": "react"
7 | },
8 | "exclude": [
9 | "node_modules"
10 | ]
11 | }
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | const loaderUtils = require('loader-utils');
2 | const { replaceImport } = require('./util');
3 |
4 | module.exports = function (source) {
5 | const options = loaderUtils.getOptions(this);
6 | return replaceImport(source, options, this.context);
7 | };
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ui-component-loader",
3 | "version": "1.4.4",
4 | "main": "./lib/index.js",
5 | "engines": {
6 | "node": ">= 6.0.0"
7 | },
8 | "scripts": {
9 | "test": "mocha test"
10 | },
11 | "dependencies": {
12 | "loader-utils": "^1.0.0",
13 | "webpack": "^4"
14 | },
15 | "devDependencies": {
16 | "mocha": "^3.5.3"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ui-component-loader-demo",
3 | "version": "0.0.1",
4 | "scripts": {
5 | "start": "webpack-dev-server",
6 | "build": "webpack"
7 | },
8 | "dependencies": {
9 | "antd": "^3.7.3",
10 | "react": "^16.4.2",
11 | "react-dom": "^16.4.2"
12 | },
13 | "devDependencies": {
14 | "@types/react": "^16.4.7",
15 | "@types/react-dom": "^16.0.6",
16 | "css-loader": "^1.0.0",
17 | "style-loader": "^0.21.0",
18 | "ts-loader": "^4.4.2",
19 | "typescript": "^2.9.2",
20 | "ui-component-loader": "^1.1.5",
21 | "webpack": "^4.16.4",
22 | "webpack-cli": "^3.1.0",
23 | "webpack-dev-server": "^3.1.5"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/demo/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | entry: './src/index.tsx',
5 | output: {
6 | filename: './bundle.js',
7 | },
8 | resolve: {
9 | extensions: ['.js', '.tsx', '.ts']
10 | },
11 | module: {
12 | rules: [
13 | {
14 | test: /\.tsx?$/,
15 | use: [
16 | 'ts-loader',
17 | {
18 | loader: path.resolve(__dirname, '../lib'),
19 | options: {
20 | 'lib': 'antd',
21 | 'camel2': '-',
22 | 'libDir': 'es',
23 | 'style': 'style/index.css',
24 | }
25 | },
26 | ],
27 | // 你导入了antd所在的源码的位置
28 | include: path.resolve('src'),
29 | },
30 | {test: /\.css?$/, use: ['style-loader', 'css-loader']}
31 | ]
32 | },
33 | devtool: 'source-map'
34 | };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://www.npmjs.com/package/ui-component-loader)
2 | [](https://travis-ci.org/gwuhaolin/ui-component-loader)
3 | [](https://www.npmjs.com/package/ui-component-loader)
4 |
5 | # ui-component-loader
6 | Modular UI component loader for webpack, a good alternative for [babel-plugin-import](https://github.com/ant-design/babel-plugin-import).
7 |
8 | Compatible with [antd](https://github.com/ant-design/ant-design), [antd-mobile](https://github.com/ant-design/ant-design-mobile), and so on.
9 |
10 | ### Example
11 |
12 | | description | options | source | output |
13 | | --- | --- | --- | --- |
14 | | replace one | `lib: 'antd'` | `import {Button} from 'antd'` | `import Button from 'antd/lib/Button'` |
15 | | replace many | `lib: 'antd'` | `import {Button,Icon} from 'antd'` | `import Button from 'antd/lib/Button' import Icon from 'antd/lib/Icon'` |
16 | | set libDir | `lib: 'antd', libDir: 'es'` | `import {Button} from 'antd'` | `import Button from 'antd/es/Button'` |
17 | | use style | `lib: 'antd', style: 'index.css'` | `import {Button} from 'antd'` | `import Button from 'antd/lib/Button' import 'antd/lib/Button/index.css'` |
18 | | translate camel | `lib: 'antd', camel2: '-'` | `import {MyComponent} from 'antd'` | `import MyComponent from 'antd/lib/my-component'` |
19 | | componentDirMap | `lib: 'antd', camel2: '-',componentDirMap: {MyComponent: 'YourComponent'}` | `import {MyComponent} from 'antd'` | `import MyComponent from 'antd/lib/YourComponent'` |
20 |
21 |
22 | ### Usage
23 | Install by:
24 | ```bash
25 | npm install ui-component-loader -D
26 | ```
27 |
28 | Edit `webpack.config.js`:
29 | ```js
30 | module.exports = {
31 | module: {
32 | rules: [
33 | {
34 | test: /\.tsx?$/,
35 | use: [
36 | 'ts-loader',
37 | {
38 | loader: 'ui-component-loader',
39 | options: {
40 | 'lib': 'antd',
41 | 'camel2': '-',
42 | 'style': 'style/css.js',
43 | }
44 | },
45 | ],
46 | // the path you import antd
47 | include: path.resolve('src/client'),
48 | },
49 | ]
50 | },
51 | };
52 | ```
53 | > The order of loaders has no require, but the source code pass to ui-component-loader must use ES6 module syntax.
54 |
55 | If you have mutil package need to be spilt, can get options as a array like:
56 | ```js
57 | {
58 | loader: 'ui-component-loader',
59 | options: {
60 | antd: {
61 | 'camel2': '-',
62 | 'style': 'style/css.js',
63 | },
64 | mui: {
65 | },
66 | }
67 | }
68 | ```
69 |
70 | ## Options
71 | - **lib**: library want to replace,value is name in npm.
72 | - **libDir**: library directory path which store will use code, default it `lib`
73 | - **style**: should append style file to a component? value is relative path to style file.
74 | - **camel2**: should translate MyComponent to my-component, value is the join string.
75 | - **existCheck**: should check if import file exist, only import file when exits, default will check. To close this check set existCheck to `(componentDirPath)=> true`.
76 | - **componentDirMap**: a map to store Component Entry Dir in lib by ComponentName, it's structure should be `{ComponentName:ComponentDir}`,default is `{}`.
77 |
78 | ## Diff with babel-plugin-import
79 | 1. babel-plugin-import is a babel plugin, which means must be used in project with babel.
80 | But in some project, like project use TypeScript, ui-component-loader is useful,
81 | ui-component-loader **can be used in any project with webpack**.
82 | 2. ui-component-loader **has better performance** as it's implement by regular expression replace string, but babel-plugin-import base on AST.
83 |
84 |
--------------------------------------------------------------------------------
/lib/util.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const fs = require("fs");
3 | // 用于匹配 import { Button } from 'antd';
4 | const importReg = /import\s+{\s*([A-Za-z0-9,\n/\\* ]+)\s*}\s+from\s+['"](\S+)['"];?/g;
5 |
6 | /**
7 | * translate ComponentName to component-name | component_name
8 | * @param componentName
9 | * @param joinString - or _
10 | * @returns {string} component-name | component_name
11 | */
12 | function tranCamel2(componentName, joinString) {
13 | if (typeof joinString === "string") {
14 | return componentName
15 | .replace(/([a-z])([A-Z])/g, `$1${joinString}$2`)
16 | .toLowerCase();
17 | }
18 | return componentName;
19 | }
20 |
21 | /**
22 | * replace backward slash (\) with forward slash (/)
23 | * @param {string} filePath path
24 | */
25 | function winPath(filePath) {
26 | // for windows
27 | if (process.platform === "win32") {
28 | return filePath.replace(/\\/g, "/");
29 | }
30 | return filePath;
31 | }
32 |
33 | function resolveNodeModule(moduleName, context) {
34 | let ret = path.resolve(context, "node_modules", moduleName);
35 | if (fs.existsSync(ret)) {
36 | return ret;
37 | } else {
38 | return resolveNodeModule(moduleName, path.resolve(context, '..'))
39 | }
40 | }
41 |
42 | /**
43 | * file exists in node_modules?
44 | */
45 | function fileExistInNpm(moduleName, filePath, context) {
46 | const p = path.resolve(resolveNodeModule(moduleName, context), filePath);
47 | return fs.existsSync(p) || fs.existsSync(p + '.js');
48 | }
49 |
50 | // 从需要按需加载的npm包的根目录读取ui-component-loader.json文件作为该包的componentDirMap配置
51 | function getComponentDirMap(libName, context) {
52 | const modulePath = resolveNodeModule(libName, context);
53 | if (!modulePath) {
54 | return {};
55 | }
56 | const mapFilePath = path.resolve(modulePath, `ui-component-loader.json`);
57 | if (fs.existsSync(mapFilePath)) {
58 | return require(mapFilePath);
59 | }
60 | return {};
61 | }
62 |
63 | // 从用户传入的options中获取指定包名的配置,如果该包没有对应的配置就访问空
64 | function getOptionForLib(libName, options) {
65 | if (!libName || !options) {
66 | return;
67 | }
68 | let ret = options[libName];
69 | if (typeof ret === "object" && ret != null) {
70 | return Object.assign(
71 | {
72 | lib: libName
73 | },
74 | ret
75 | );
76 | }
77 | if (options.lib === libName) {
78 | return options;
79 | }
80 | }
81 |
82 | function replaceImport(source, options = {}, context) {
83 | return source.replace(importReg, function(org, importComponents, importFrom) {
84 | const option = getOptionForLib(importFrom, options);
85 | if (!option) {
86 | // 该包没有配置,不做转换处理
87 | return org;
88 | }
89 | const {
90 | lib, // lib name
91 | libPolyfill,
92 | libDir = "lib", //lib dir in npm
93 | style, // style file path
94 | camel2, // translate ComponentName to component-name | component_name
95 | existCheck = fileExistInNpm, // only import file when exits
96 | componentDirMap = getComponentDirMap(lib, context) // used to map a ComponentName to ComponentEntryPath in lib
97 | } = option;
98 | // 需要导入的组件列表
99 | importComponents = removeComment(importComponents).split(",");
100 | let ret = "";
101 | importComponents.forEach(function(componentName) {
102 | // 如果组件名称为空就直接忽略
103 | componentName = componentName.trim();
104 | if (componentName.length === 0) {
105 | return;
106 | }
107 |
108 | // 组件的入口目录路径
109 | let componentDirPath;
110 |
111 | if (typeof componentDirMap[componentName] === "string") {
112 | // 如果在 map 配置了针对这个组件名称的入口目录映射,就优先采用映射
113 | componentDirPath = path.join(libDir, componentDirMap[componentName]);
114 | } else {
115 | componentDirPath = path.join(libDir, tranCamel2(componentName, camel2));
116 | }
117 |
118 | function injectComponent(importFrom) {
119 | // 当文件存在时才导入
120 | // 导入组件入口 JS 文件
121 | ret += `import ${componentName} from '${path.join(
122 | importFrom,
123 | componentDirPath
124 | )}';`;
125 | if (style) {
126 | // style 文件入口
127 | const styleFilePath = path.join(componentDirPath, style);
128 | if (existCheck(importFrom, styleFilePath, context)) {
129 | // 导入组件 CSS 入口文件
130 | ret += `import '${path.join(
131 | importFrom,
132 | styleFilePath
133 | )}';`;
134 | }
135 | }
136 | }
137 |
138 | if (existCheck(importFrom, componentDirPath, context)) {
139 | injectComponent(importFrom);
140 | } else if (libPolyfill && existCheck(libPolyfill, componentDirPath, context)) {
141 | injectComponent(libPolyfill);
142 | }
143 | });
144 | return winPath(ret);
145 | });
146 | }
147 |
148 | // 删除JS代码中的注释
149 | function removeComment(sourceCode) {
150 | return sourceCode.replace(/\/\/.*\n/g, "").replace(/\/\*(\s|.)*\*\//g, "");
151 | }
152 |
153 | module.exports = {
154 | replaceImport,
155 | removeComment,
156 | tranCamel2
157 | };
158 |
--------------------------------------------------------------------------------
/test/index.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const assert = require('assert');
3 | const {replaceImport, removeComment, tranCamel2} = require('../lib/util');
4 |
5 | describe('util.js#replaceImport', function () {
6 |
7 | const testData = [
8 | {
9 | des: '没有 lib 参数',
10 | source: `import {Button} from 'antd'`,
11 | output: `import {Button} from 'antd'`,
12 | options: undefined,
13 | },
14 | {
15 | des: 'lib 不符合',
16 | source: `import {Component} from 'react'`,
17 | output: `import {Component} from 'react'`,
18 | options: {
19 | lib: 'antd'
20 | },
21 | },
22 | {
23 | des: 'lib 命中,保留后面的;',
24 | source: `import {Button} from 'antd';`,
25 | output: `import Button from 'antd/lib/Button';`,
26 | options: {
27 | lib: 'antd'
28 | },
29 | },
30 | {
31 | des: 'lib 命中',
32 | source: `import {Button} from 'antd'`,
33 | output: `import Button from 'antd/lib/Button';`,
34 | options: {
35 | lib: 'antd'
36 | },
37 | },
38 | {
39 | des: '使用 libDir',
40 | source: `import {Button} from 'antd'`,
41 | output: `import Button from 'antd/es/Button';`,
42 | options: {
43 | lib: 'antd',
44 | libDir: 'es',
45 | },
46 | },
47 | {
48 | des: 'import 语句中间有很多空格',
49 | source: `import { Button} from "antd"`,
50 | output: `import Button from 'antd/lib/Button';`,
51 | options: {
52 | lib: 'antd'
53 | },
54 | },
55 | {
56 | des: '导入多个 component',
57 | source: `import { Button, Icon} from "antd"`,
58 | output: `import Button from 'antd/lib/Button';import Icon from 'antd/lib/Icon';`,
59 | options: {
60 | lib: 'antd'
61 | },
62 | },
63 | {
64 | des: '使用 style',
65 | source: `import {Button} from 'antd';`,
66 | output: `import Button from 'antd/lib/Button';import 'antd/lib/Button/index.css';`,
67 | options: {
68 | lib: 'antd',
69 | style: 'index.css'
70 | },
71 | },
72 | {
73 | des: '导入多个 component & 使用 style',
74 | source: `import {Button,Icon} from 'antd'`,
75 | output: `import Button from 'antd/lib/Button';import 'antd/lib/Button/index.css';import Icon from 'antd/lib/Icon';import 'antd/lib/Icon/index.css';`,
76 | options: {
77 | lib: 'antd',
78 | style: 'index.css'
79 | },
80 | },
81 | {
82 | des: '使用 相对路径的 style',
83 | source: `import {Button} from 'antd';`,
84 | output: `import Button from 'antd/lib/Button';import 'antd/lib/Button/style/index.css';`,
85 | options: {
86 | lib: 'antd',
87 | style: './style/index.css'
88 | },
89 | },
90 | {
91 | des: '使用 camel2 转化大小写',
92 | source: `import {MyComponent} from 'antd'`,
93 | output: `import MyComponent from 'antd/lib/my-component';`,
94 | options: {
95 | lib: 'antd',
96 | camel2: '-'
97 | },
98 | },
99 | {
100 | des: '使用 camel2 转化大小写',
101 | source: `import {Button} from 'antd'`,
102 | output: `import Button from 'antd/lib/button';`,
103 | options: {
104 | lib: 'antd',
105 | camel2: '-'
106 | },
107 | },
108 | {
109 | des: '使用 componentDirMap 映射路径',
110 | source: `import {MyComponent} from 'antd'`,
111 | output: `import MyComponent from 'antd/lib/YourComponent';`,
112 | options: {
113 | lib: 'antd',
114 | camel2: '-',
115 | componentDirMap: {
116 | MyComponent: 'YourComponent'
117 | }
118 | },
119 | },
120 | {
121 | des: '不能覆盖其它',
122 | source: `import * as React from 'react';
123 | import {Component} from 'react';
124 | import {svgQRCode} from '@mtfe/mcashier-components/es/Icon/svgs';`,
125 | output: `import * as React from 'react';
126 | import {Component} from 'react';
127 | import {svgQRCode} from '@mtfe/mcashier-components/es/Icon/svgs';`,
128 | options: {
129 | lib: '@mtfe/mcashier-components',
130 | libDir: 'es',
131 | },
132 | },
133 | {
134 | des: '不能覆盖其它2',
135 | source: `import * as React from 'react';
136 | import {Component} from 'react';
137 | import {svgQRCode} from '@mtfe/mcashier-components/es/Icon/svgs';
138 | import {Form} from '@mtfe/mcashier-components';
139 | import {GoodsTO} from '@server/thrift/dist/GoodsModel_types';`,
140 | output: `import * as React from 'react';
141 | import {Component} from 'react';
142 | import {svgQRCode} from '@mtfe/mcashier-components/es/Icon/svgs';
143 | import Form from '@mtfe/mcashier-components/es/Form';
144 | import {GoodsTO} from '@server/thrift/dist/GoodsModel_types';`,
145 | options: {
146 | lib: '@mtfe/mcashier-components',
147 | libDir: 'es',
148 | },
149 | },
150 |
151 | {
152 | des: '支持组件换行',
153 | source: `import {
154 | DatePicker,
155 | List,
156 | } from '@mtfe/mcashier-components'`,
157 | output: `import DatePicker from '@mtfe/mcashier-components/es/DatePicker';import List from '@mtfe/mcashier-components/es/List';`,
158 | options: {
159 | lib: '@mtfe/mcashier-components',
160 | libDir: 'es',
161 | },
162 | },
163 | {
164 | des: '支持组件换行后注释掉部分组件',
165 | source: `import {
166 | toast
167 | // IDateRangePickerViewProps,
168 | /* Aa */
169 | } from '@mtfe/sjst-ui'`,
170 | output: `import toast from '@mtfe/sjst-ui/es/Toast';import '@mtfe/sjst-ui/es/Toast/index.css';`,
171 | options: {
172 | lib: '@mtfe/sjst-ui',
173 | libDir: 'es',
174 | style: 'index.css',
175 | componentDirMap: {
176 | toast: 'Toast',
177 | },
178 | },
179 | },
180 | ];
181 |
182 | testData.forEach(({des, source, output, options = {}}) => {
183 | it(des, function () {
184 | let realOutput = replaceImport(source, Object.assign(options, {
185 | existCheck: () => true,
186 | }));
187 | assert.equal(realOutput, output, `
188 | source=${source}
189 | options=${JSON.stringify(options)}
190 | `);
191 | });
192 | });
193 | });
194 |
195 | describe('util.js#removeComment', () => {
196 | it('removeComment', () => {
197 | const ret = removeComment(`import {
198 | //a
199 | // a
200 | /*a*/
201 | /*a
202 | * a
203 | * */
204 | a
205 | } from 'a'
206 | `);
207 | assert.equal(ret, `import {
208 |
209 | a
210 | } from 'a'
211 | `);
212 | });
213 | });
214 |
215 | describe('util.js#tranCamel2', () => {
216 | it('joinString is null', () => {
217 | assert.equal(tranCamel2('InputItem'), 'InputItem');
218 | });
219 | it('joinString is -', () => {
220 | assert.equal(tranCamel2('InputItem', '-'), 'input-item');
221 | });
222 | it('joinString is _', () => {
223 | assert.equal(tranCamel2('InputItem', '_'), 'input_item');
224 | });
225 | it('joinString is blank str', () => {
226 | assert.equal(tranCamel2('InputItem', ''), 'inputitem');
227 | });
228 | it('1', () => {
229 | assert.equal(tranCamel2('Input', '_'), 'input');
230 | });
231 | it('3', () => {
232 | assert.equal(tranCamel2('InputItemIcon', '_'), 'input_item_icon');
233 | });
234 | it('4', () => {
235 | assert.equal(tranCamel2('InputItemIconButton', '_'), 'input_item_icon_button');
236 | });
237 | });
--------------------------------------------------------------------------------