├── test ├── src │ ├── fixture.wxml │ ├── fixture.gif │ └── images │ │ └── image.gif ├── config.js └── test.js ├── .eslintrc ├── .gitignore ├── .babelrc ├── .travis.yml ├── .circleci └── config.yml ├── .editorconfig ├── appveyor.yml ├── LICENSE.md ├── package.json ├── README.md └── src └── index.js /test/src/fixture.wxml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["cantonjs"], 3 | } 4 | -------------------------------------------------------------------------------- /test/src/fixture.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cap32/wxml-loader/HEAD/test/src/fixture.gif -------------------------------------------------------------------------------- /test/src/images/image.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cap32/wxml-loader/HEAD/test/src/images/image.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .DS_Store? 3 | ._* 4 | .Spotlight-V100 5 | .Trashes 6 | ehthumbs.db 7 | Thumbs.db 8 | *.log 9 | .log/ 10 | .logs/ 11 | node_modules/ 12 | lib/ 13 | coverage/ 14 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "stage-0", 4 | ], 5 | "plugins": [ 6 | ["transform-es2015-destructuring", { "loose": false }], 7 | ["transform-es2015-modules-commonjs", { "loose": false }], 8 | "transform-es2015-parameters", 9 | ], 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "6" 5 | - "8" 6 | - "10" 7 | 8 | install: 9 | - npm install 10 | 11 | after_script: 12 | - npm run coverage 13 | 14 | notifications: 15 | email: 16 | on_success: never 17 | on_failure: always 18 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | build: 5 | docker: 6 | - image: node:10.0 7 | 8 | steps: 9 | - checkout 10 | 11 | - run: 12 | name: Nodejs Version 13 | command: node --version 14 | 15 | - run: 16 | name: Install Packages 17 | command: yarn install 18 | 19 | - run: 20 | name: Test Packages 21 | command: yarn test 22 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | 9 | [*.{js,jsx,css,less,scss}] 10 | indent_style = tab 11 | tab_width = 2 12 | trim_trailing_whitespace = true 13 | 14 | [*.{html,hbr,rt,sass}] 15 | indent_style = space 16 | indent_size = 2 17 | trim_trailing_whitespace = true 18 | 19 | [*.json] 20 | indent_style = space 21 | indent_size = 2 22 | trim_trailing_whitespace = true 23 | 24 | [*.{md,yml}] 25 | trim_trailing_whitespace = false 26 | indent_size = 2 27 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # fix line endings on Windows and checkout all strings with \r\n 2 | init: 3 | - git config --global core.autocrlf true 4 | 5 | # Test against this version of Node.js 6 | environment: 7 | matrix: 8 | # node.js 9 | - nodejs_version: "10" 10 | - nodejs_version: "8" 11 | - nodejs_version: "6" 12 | 13 | platform: 14 | - x86 15 | - x64 16 | 17 | # Install scripts. (runs after repo cloning) 18 | install: 19 | # Get the latest stable version of Node.js or io.js 20 | - ps: Install-Product node $env:nodejs_version 21 | # install modules 22 | - npm i 23 | 24 | # Post-install test scripts. 25 | test_script: 26 | # Output useful info for debugging 27 | - node --version && npm --version 28 | # run tests 29 | - npm test 30 | 31 | # Don't actually build. 32 | build: off 33 | 34 | matrix: 35 | fast_finish: true 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Webb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /test/config.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | 3 | const srcDir = resolve(__dirname, 'src'); 4 | 5 | export default (options = {}) => { 6 | const { target, globalPublicPath = '/', ...wxmlLoaderOptions } = options; 7 | return { 8 | entry: resolve(__dirname, 'src', 'index.wxml'), 9 | mode: 'development', 10 | output: { 11 | filename: 'index.js', 12 | publicPath: globalPublicPath, 13 | path: resolve(__dirname, 'dist'), 14 | }, 15 | target, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.wxml$/, 20 | use: [ 21 | { 22 | loader: 'file-loader', 23 | options: { 24 | name: '[name].[ext]', 25 | useRelativePath: true, 26 | context: srcDir, 27 | }, 28 | }, 29 | { 30 | loader: './src', 31 | options: { 32 | root: srcDir, 33 | ...wxmlLoaderOptions, 34 | }, 35 | }, 36 | ], 37 | }, 38 | { 39 | test: /\.gif$/, 40 | use: [ 41 | { 42 | loader: 'file-loader', 43 | options: { 44 | name: '[name].[ext]', 45 | useRelativePath: true, 46 | context: srcDir, 47 | }, 48 | }, 49 | ], 50 | }, 51 | ], 52 | }, 53 | stats: 'verbose', 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wxml-loader", 3 | "description": "wxml loader for webpack", 4 | "version": "0.3.0", 5 | "main": "lib/index", 6 | "files": [ 7 | "*.md", 8 | "bin", 9 | "lib" 10 | ], 11 | "scripts": { 12 | "start": "jest test --watch", 13 | "test": "jest test --coverage", 14 | "coverage": "cat ./coverage/lcov.info | coveralls", 15 | "prettier": "prettier-eslint --write \"+(src|test)/**/*.js\"", 16 | "prebuild": "rimraf lib", 17 | "build:watch": "babel src -d lib -w", 18 | "build": "babel src -d lib", 19 | "preversion": "yarn test && yarn build" 20 | }, 21 | "author": "cap32", 22 | "repository": "cap32/wxml-loader", 23 | "keywords": [ 24 | "wxml", 25 | "loader", 26 | "webpack", 27 | "wxapp", 28 | "weapp", 29 | "mini-program", 30 | "wechat", 31 | "alipay" 32 | ], 33 | "licenses": "MIT", 34 | "dependencies": { 35 | "html-minifier": "^3.5.6", 36 | "loader-utils": "^1.1.0", 37 | "sax": "^1.2.2" 38 | }, 39 | "devDependencies": { 40 | "babel-cli": "^6.11.4", 41 | "babel-core": "^6.13.2", 42 | "babel-jest": "^21.2.0", 43 | "babel-plugin-transform-es2015-destructuring": "^6.19.0", 44 | "babel-plugin-transform-es2015-modules-commonjs": "^6.18.0", 45 | "babel-plugin-transform-es2015-parameters": "^6.18.0", 46 | "babel-polyfill": "^6.13.0", 47 | "babel-preset-stage-0": "^6.5.0", 48 | "babel-register": "^6.11.6", 49 | "coveralls": "^3.0.0", 50 | "cross-env": "^4.0.0", 51 | "eslint": "^5.4.0", 52 | "eslint-config-cantonjs": "^2.0.0", 53 | "eslint-config-standard": "^11.0.0", 54 | "eslint-plugin-import": "^2.14.0", 55 | "eslint-plugin-jest": "^21.22.0", 56 | "eslint-plugin-node": "^7.0.1", 57 | "eslint-plugin-promise": "^4.0.0", 58 | "eslint-plugin-react": "^7.11.1", 59 | "eslint-plugin-standard": "^3.1.0", 60 | "file-loader": "^1.1.11", 61 | "jest": "^21.2.1", 62 | "mkdirp": "^0.5.1", 63 | "prettier-eslint-cli": "^4.7.1", 64 | "rimraf": "^2.5.4", 65 | "webpack": "^4.16.0" 66 | }, 67 | "jest": { 68 | "collectCoverageFrom": [ 69 | "src/**/*.js" 70 | ], 71 | "modulePathIgnorePatterns": [ 72 | "node_modules", 73 | "lib", 74 | "dist" 75 | ] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wxml-loader 2 | 3 | [![CircleCI](https://circleci.com/gh/Cap32/wxml-loader.svg?style=shield)](https://circleci.com/gh/Cap32/wxml-loader) 4 | [![Build Status](https://travis-ci.org/Cap32/wxml-loader.svg?branch=master)](https://travis-ci.org/Cap32/wxml-loader) 5 | [![Build status](https://ci.appveyor.com/api/projects/status/kcp9grsyjd73n0lm?svg=true)](https://ci.appveyor.com/project/Cap32/wxml-loader) 6 | [![Coverage Status](https://coveralls.io/repos/github/Cap32/wxml-loader/badge.svg?branch=master)](https://coveralls.io/github/Cap32/wxml-loader?branch=master) 7 | [![npm version](https://badge.fury.io/js/wxml-loader.svg)](https://badge.fury.io/js/wxml-loader) 8 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com) 9 | [![License](https://img.shields.io/badge/license-MIT_License-blue.svg?style=flat)](https://github.com/Cap32/wxml-loader/blob/master/LICENSE.md) 10 | 11 | wxml loader for webpack 12 | 13 | **Please note this 14 | [wxml](https://mp.weixin.qq.com/debug/wxadoc/dev/framework/view/wxml/) is a 15 | markup language for 16 | [Wechat mini programs](https://mp.weixin.qq.com/debug/wxadoc/dev/)** 17 | 18 | ## Installation 19 | 20 | ```bash 21 | yarn add -D wxml-loader 22 | ``` 23 | 24 | ## Usage 25 | 26 | You may also need to use 27 | [file-loader](https://github.com/webpack-contrib/file-loader) to extract files. 28 | 29 | ```js 30 | { 31 | test: /\.wxml$/, 32 | include: /src/, 33 | use: [ 34 | { 35 | loader: 'file-loader', 36 | options: { 37 | name: '[name].[ext]', 38 | useRelativePath: true, 39 | context: resolve('src'), 40 | }, 41 | }, 42 | { 43 | loader: 'wxml-loader', 44 | options: { 45 | root: resolve('src'), 46 | enforceRelativePath: true, 47 | }, 48 | }, 49 | ], 50 | } 51 | ``` 52 | 53 | ##### Options 54 | 55 | * `root` (String): Root path for requiring sources 56 | * `enforceRelativePath` (Boolean): Should be true if you wish to generate a 57 | `root` relative URL for each file. **It is recommend to set to `true`** 58 | * `publicPath` (String): Defaults to webpack 59 | [output.publicPath](https://webpack.js.org/configuration/output/#output-publicpath) 60 | * `transformContent(content, resource)` (Function): Transform content, should 61 | return a content string 62 | * `transformUrl(url, resource)` (Function): Transform url, should return a url 63 | * `minimize` (Boolean): To minimize. Defaults to `false` 64 | * All 65 | [html-minifier](https://github.com/kangax/html-minifier#options-quick-reference) 66 | options are supported 67 | 68 | ## Known Issues 69 | 70 | Currently `wxml-loader` could not resolve dynamic path, i.e. 71 | ``. Please use `copy-webapck-plugin` to 72 | copy those resource to dist directory manually. See 73 | https://github.com/Cap32/wxml-loader/issues/1 for detail (Chinese). 74 | 75 | ## For Alipay mini programs 76 | 77 | This loader is also compatible with 78 | [Alipay mini programs](https://mini.open.alipay.com/channel/miniIndex.htm). You 79 | just need to make sure using `test: /\.axml$/` instead of `test: /\.wxml$/` in 80 | webpack config. 81 | 82 | If you're using 83 | [wxapp-webpack-plugin](https://github.com/Cap32/wxapp-webpack-plugin) and 84 | setting `Targets.Alipay` as webpack target, it will automatically set 85 | `transformContent()` and `transformUrl()` option by default, the 86 | `transformContent()` function will transform `wx:attr` attribute to `a:attr`, 87 | and the `transformUrl()` function will transform `.wxml` extension to `.axml` 88 | automatically. That means you could write mini programs once, and build both 89 | Wechat and Alipay mini programs. 90 | 91 | ###### Example 92 | 93 | webpack.config.babel.js 94 | 95 | ```js 96 | import WXAppWebpackPlugin, { Targets } from "wxapp-webpack-plugin"; 97 | export default env => ({ 98 | // ...other 99 | target: Targets[env.target || "Wechat"], 100 | module: { 101 | rules: [ 102 | // ...other, 103 | { 104 | test: /\.wxml$/, 105 | use: [ 106 | { 107 | loader: "file-loader", 108 | options: { 109 | name: `[name].${env.target === "Alipay" ? "axml" : "wxml"}` 110 | useRelativePath: true, 111 | context: resolve('src'), 112 | }, 113 | }, 114 | { 115 | loader: 'wxml-loader', 116 | options: { 117 | root: resolve('src'), 118 | enforceRelativePath: true, 119 | }, 120 | }, 121 | ] 122 | } 123 | ] 124 | }, 125 | plugin: [ 126 | // ...other 127 | new WXAppWebpackPlugin() 128 | ] 129 | }); 130 | ``` 131 | 132 | ## Related 133 | 134 | For a complete guild to use `webpack` to develop `WeiXin App`, please checkout 135 | my [wxapp-boilerplate](https://github.com/cantonjs/wxapp-boilerplate) repo. 136 | 137 | ## License 138 | 139 | MIT 140 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import rimraf from 'rimraf'; 3 | import { readFileSync, writeFileSync } from 'fs'; 4 | import webpack from 'webpack'; 5 | import config from './config'; 6 | 7 | const getCompiledRes = () => 8 | readFileSync(resolve(__dirname, 'dist', 'index.wxml'), 'utf8'); 9 | 10 | const writeWXML = (content) => 11 | writeFileSync(resolve(__dirname, 'src', 'index.wxml'), content, 'utf8'); 12 | 13 | const clear = () => { 14 | rimraf.sync(resolve(__dirname, 'src', 'index.wxml')); 15 | rimraf.sync(resolve(__dirname, 'dist')); 16 | }; 17 | 18 | const target = (target) => (compiler) => 19 | compiler.apply(new webpack.LoaderTargetPlugin(target)); 20 | 21 | const mkdir = () => { 22 | clear(); 23 | }; 24 | 25 | const compile = (content, options = {}) => { 26 | writeWXML(content); 27 | return new Promise((resolve, reject) => { 28 | webpack(config(options), (err, stats) => { 29 | if (err || stats.hasErrors()) { 30 | reject(err || JSON.stringify(stats.toJson('errors-only'))); 31 | } 32 | else { 33 | resolve(); 34 | } 35 | }); 36 | }); 37 | }; 38 | 39 | describe('wxml-loader', async () => { 40 | beforeEach(mkdir); 41 | afterEach(clear); 42 | 43 | test('should export file', async () => { 44 | const content = ''; 45 | await compile(content); 46 | expect(getCompiledRes()).toBe(content); 47 | }); 48 | 49 | test('should minimize file', async () => { 50 | await compile(' ', { minimize: true }); 51 | expect(getCompiledRes()).toBe(''); 52 | }); 53 | 54 | test('should minimize work with self closing element', async () => { 55 | await compile('', { minimize: true }); 56 | expect(getCompiledRes()).toBe(''); 57 | }); 58 | 59 | test('should src work', async () => { 60 | await compile(''); 61 | expect(getCompiledRes()).toBe(''); 62 | }); 63 | 64 | test('should root optinal', async () => { 65 | const content = ''; 66 | await compile(content, { root: undefined }); 67 | expect(getCompiledRes()).toBe(''); 68 | }); 69 | 70 | test('should dynamic src not work', async () => { 71 | await compile(''); 72 | expect(getCompiledRes()).toBe(''); 73 | }); 74 | 75 | test('should Wechat target work', async () => { 76 | await compile( 77 | '{{item}}', 78 | { target: target(function Wechat() {}) }, 79 | ); 80 | expect(getCompiledRes()).toBe( 81 | '{{item}}', 82 | ); 83 | }); 84 | 85 | test('should Alipay target work', async () => { 86 | await compile( 87 | '{{item}}', 88 | { target: target(function Alipay() {}) }, 89 | ); 90 | expect(getCompiledRes()).toBe( 91 | '{{item}}', 92 | ); 93 | }); 94 | 95 | test('should transformContent() work', async () => { 96 | await compile(' {{item}} ', { 97 | target: target(function Alipay() {}), 98 | transformContent: (content) => content.replace(/\bwx:/, '🦄:'), 99 | }); 100 | expect(getCompiledRes()).toBe(' {{item}} '); 101 | }); 102 | 103 | // DEPRECATED 104 | test('should format() work', async () => { 105 | await compile(' {{item}} ', { 106 | target: target(function Alipay() {}), 107 | format: (content) => content.replace(/\bwx:/, '🦄:'), 108 | }); 109 | expect(getCompiledRes()).toBe(' {{item}} '); 110 | }); 111 | 112 | test('should transformUrl() work', async () => { 113 | await compile('', { 114 | target: target(function Alipay() {}), 115 | transformUrl: (url) => url.replace(/fixture/, '🦄'), 116 | }); 117 | expect(getCompiledRes()).toBe(''); 118 | }); 119 | 120 | test('should minimize work with ', async () => { 121 | const code = '
'; 122 | await compile(code, { minimize: true }); 123 | expect(getCompiledRes()).toBe('
'); 124 | }); 125 | 126 | test('should minimize work with camelCase attribute', async () => { 127 | const code = '
'; 128 | await compile(code, { minimize: true }); 129 | expect(getCompiledRes()).toBe('
'); 130 | }); 131 | 132 | test('should minimize work with form attribute', async () => { 133 | const code = '
'; 134 | await compile(code, { minimize: true }); 135 | expect(getCompiledRes()).toBe( 136 | '
', 137 | ); 138 | }); 139 | 140 | test('should minimize work with controls attribute', async () => { 141 | const code = ''; 142 | await compile(code, { minimize: true }); 143 | expect(getCompiledRes()).toBe( 144 | '', 145 | ); 146 | }); 147 | 148 | test('should ensure url if not absolute and not starts with a dot', async () => { 149 | const code = ''; 150 | await compile(code, { globalPublicPath: '' }); 151 | expect(getCompiledRes()).toBe(''); 152 | }); 153 | 154 | test('should enforceRelativePath work', async () => { 155 | const code = ''; 156 | await compile(code, { enforceRelativePath: true }); 157 | expect(getCompiledRes()).toBe(''); 158 | }); 159 | 160 | test('should work if publicPath starts with protocol', async () => { 161 | const code = ''; 162 | await compile(code, { globalPublicPath: 'http://m.baidu.com/' }); 163 | expect(getCompiledRes()).toBe( 164 | '', 165 | ); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { resolve, isAbsolute, relative, dirname, join } from 'path'; 2 | import sax from 'sax'; 3 | import { Script } from 'vm'; 4 | import Minifier from 'html-minifier'; 5 | import { isUrlRequest, urlToRequest, getOptions } from 'loader-utils'; 6 | 7 | const ROOT_TAG_NAME = 'xxx-wxml-root-xxx'; 8 | const ROOT_TAG_START = `<${ROOT_TAG_NAME}>`; 9 | const ROOT_TAG_END = ``; 10 | const ROOT_TAG_LENGTH = ROOT_TAG_START.length; 11 | 12 | const isSrc = (name) => name === 'src'; 13 | 14 | const isDynamicSrc = (src) => /\{\{/.test(src); 15 | 16 | const isStartsWithDot = (src) => /^\./.test(src); 17 | 18 | const hasProcotol = (src) => /^(\w+:)?\/\//.test(src); 19 | 20 | const replaceAt = (str, start, end, replacement) => 21 | str.slice(0, start) + replacement + str.slice(end); 22 | 23 | const extract = (src, __webpack_public_path__) => { 24 | const script = new Script(src, { displayErrors: true }); 25 | const sandbox = { 26 | __webpack_public_path__, 27 | module: {}, 28 | }; 29 | script.runInNewContext(sandbox); 30 | return sandbox.module.exports.toString(); 31 | }; 32 | 33 | const defaultMinimizeConf = { 34 | caseSensitive: true, 35 | html5: true, 36 | removeComments: true, 37 | removeCommentsFromCDATA: true, 38 | removeCDATASectionsFromCDATA: true, 39 | collapseWhitespace: true, 40 | removeRedundantAttributes: true, 41 | removeEmptyAttributes: true, 42 | keepClosingSlash: true, 43 | removeScriptTypeAttributes: true, 44 | removeStyleLinkTypeAttributes: true, 45 | }; 46 | 47 | /** 48 | * Retrieves the public path from the loader options, context.options (webpack <4) or context._compilation (webpack 4+). 49 | * context._compilation is likely to get removed in a future release, so this whole function should be removed then. 50 | * See: https://github.com/peerigon/extract-loader/issues/35 51 | * 52 | * @deprecated 53 | * @param {Object} options - Extract-loader options 54 | * @param {Object} context - Webpack loader context 55 | * @returns {string} 56 | */ 57 | function getPublicPath(options, context) { 58 | const property = 'publicPath'; 59 | if (property in options) { 60 | return options[property]; 61 | } 62 | if ( 63 | context.options && 64 | context.options.output && 65 | property in context.options.output 66 | ) { 67 | return context.options.output[property]; 68 | } 69 | if ( 70 | context._compilation && 71 | context._compilation.outputOptions && 72 | property in context._compilation.outputOptions 73 | ) { 74 | return context._compilation.outputOptions[property]; 75 | } 76 | return ''; 77 | } 78 | 79 | export default function (content) { 80 | this.cacheable && this.cacheable(); 81 | 82 | const callback = this.async(); 83 | const { options: webpackLegacyOptions, _module = {}, resourcePath } = this; 84 | const { context, target } = webpackLegacyOptions || this; 85 | 86 | const options = getOptions(this) || {}; 87 | 88 | const { resource } = _module; 89 | 90 | const hasIssuer = _module.issuer; 91 | const issuerContext = (hasIssuer && _module.issuer.context) || context; 92 | 93 | const { 94 | root = resolve(context, issuerContext), 95 | publicPath = getPublicPath(options, this), 96 | enforceRelativePath = false, 97 | format, 98 | transformContent = (content) => { 99 | switch (target.name) { 100 | case 'Alipay': 101 | return content.replace(/\bwx:/g, 'a:'); 102 | case 'Wechat': 103 | return content.replace(/\ba:/g, 'wx:'); 104 | default: 105 | return content; 106 | } 107 | }, 108 | transformUrl = (url) => { 109 | switch (target.name) { 110 | case 'Alipay': 111 | return url.replace(/\.wxml$/g, '.axml'); 112 | case 'Wechat': 113 | return url.replace(/\.axml$/g, '.wxml'); 114 | default: 115 | return url; 116 | } 117 | }, 118 | minimize: forceMinimize, 119 | ...minimizeOptions 120 | } = options; 121 | 122 | const requests = []; 123 | const hasMinimzeConfig = typeof forceMinimize === 'boolean'; 124 | const shouldMinimize = hasMinimzeConfig ? forceMinimize : this.minimize; 125 | 126 | const loadModule = (request) => 127 | new Promise((resolve, reject) => { 128 | this.addDependency(request); 129 | this.loadModule(request, (err, src) => { 130 | /* istanbul ignore if */ 131 | if (err) { 132 | reject(err); 133 | } 134 | else { 135 | resolve(src); 136 | } 137 | }); 138 | }); 139 | 140 | const xmlContent = `${ROOT_TAG_START}${content}${ROOT_TAG_END}`; 141 | 142 | const ensureStartsWithDot = (source) => 143 | isStartsWithDot(source) ? source : `./${source}`; 144 | 145 | const ensureRelativePath = (source) => { 146 | const sourcePath = join(root, source); 147 | const resourceDirname = dirname(resourcePath); 148 | source = relative(resourceDirname, sourcePath).replace(/\\/g, '/'); 149 | return ensureStartsWithDot(source); 150 | }; 151 | 152 | const replaceRequest = async ({ request, startIndex, endIndex }) => { 153 | const module = await loadModule(request); 154 | let source = extract(module, publicPath); 155 | const isSourceAbsolute = isAbsolute(source); 156 | if (!isSourceAbsolute && !hasProcotol(source)) { 157 | source = ensureStartsWithDot(source); 158 | } 159 | if (enforceRelativePath && isSourceAbsolute) { 160 | source = ensureRelativePath(source); 161 | } 162 | 163 | /* istanbul ignore else */ 164 | if (typeof transformUrl === 'function') { 165 | source = transformUrl(source, resource); 166 | } 167 | content = replaceAt(content, startIndex, endIndex, source); 168 | }; 169 | 170 | const parser = sax.parser(false, { lowercase: true }); 171 | 172 | parser.onattribute = ({ name, value }) => { 173 | if ( 174 | !value || 175 | !isSrc(name) || 176 | isDynamicSrc(value) || 177 | !isUrlRequest(value, root) 178 | ) { 179 | return; 180 | } 181 | 182 | const endIndex = parser.position - 1 - ROOT_TAG_LENGTH; 183 | const startIndex = endIndex - value.length; 184 | const request = urlToRequest(value, root); 185 | 186 | requests.unshift({ request, startIndex, endIndex }); 187 | }; 188 | 189 | parser.onend = async () => { 190 | try { 191 | for (const req of requests) { 192 | await replaceRequest(req); 193 | } 194 | 195 | /* istanbul ignore else */ 196 | if (typeof format === 'function') { 197 | /* istanbul ignore else */ 198 | if (!format.__warned) { 199 | format.__warned = true; 200 | console.warn( 201 | '[DEPRECATED]: wxml-loader `format` option has been deprecated.', 202 | 'Please use `transformContent() instead`.', 203 | ); 204 | } 205 | content = format(content, resource); 206 | } 207 | else if (typeof transformContent === 'function') { 208 | content = transformContent(content, resource); 209 | } 210 | 211 | if (shouldMinimize) { 212 | content = Minifier.minify(content, { 213 | ...defaultMinimizeConf, 214 | ...minimizeOptions, 215 | }); 216 | } 217 | callback(null, content); 218 | } 219 | catch (err) { 220 | /* istanbul ignore next */ 221 | callback(err, content); 222 | } 223 | }; 224 | 225 | parser.write(xmlContent).close(); 226 | } 227 | --------------------------------------------------------------------------------