├── 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 | [![Npm Package](https://img.shields.io/npm/v/ui-component-loader.svg?style=flat-square)](https://www.npmjs.com/package/ui-component-loader) 2 | [![Build Status](https://img.shields.io/travis/gwuhaolin/ui-component-loader.svg?style=flat-square)](https://travis-ci.org/gwuhaolin/ui-component-loader) 3 | [![Npm Downloads](http://img.shields.io/npm/dm/ui-component-loader.svg?style=flat-square)](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 | }); --------------------------------------------------------------------------------