├── .eslintignore
├── .gitignore
├── .npmignore
├── demo
└── gulp-project-demo
│ ├── .gitignore
│ ├── src
│ ├── components
│ │ ├── child1
│ │ │ ├── index.json
│ │ │ ├── index.wxml
│ │ │ ├── index.module.wxss
│ │ │ └── index.js
│ │ └── child2
│ │ │ ├── index.json
│ │ │ ├── index.wxml
│ │ │ ├── index.module.scss
│ │ │ └── index.js
│ ├── app.json
│ ├── pages
│ │ └── index
│ │ │ ├── index.js
│ │ │ ├── index.json
│ │ │ ├── index.wxml
│ │ │ └── index.scss
│ ├── sitemap.json
│ ├── app.scss
│ ├── utils
│ │ └── util.js
│ ├── app.js
│ └── project.config.json
│ ├── README.md
│ ├── package.json
│ └── gulpfile.js
├── CHANGELOG.md
├── .eslintrc
├── lib
└── gulp
│ ├── remove-map.js
│ ├── postcss-deal.js
│ ├── himalayaStringifyChanged
│ ├── stringify.js
│ └── compat.js
│ ├── index.js
│ ├── replace-wxml.js
│ ├── utils.js
│ └── replace-js.js
├── LICENSE
├── package.json
└── README.md
/.eslintignore:
--------------------------------------------------------------------------------
1 | demo
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | demo
--------------------------------------------------------------------------------
/demo/gulp-project-demo/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
--------------------------------------------------------------------------------
/demo/gulp-project-demo/src/components/child1/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "component": true
3 | }
--------------------------------------------------------------------------------
/demo/gulp-project-demo/src/components/child2/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "component": true
3 | }
--------------------------------------------------------------------------------
/demo/gulp-project-demo/src/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "pages": [
3 | "pages/index/index"
4 | ]
5 | }
--------------------------------------------------------------------------------
/demo/gulp-project-demo/src/pages/index/index.js:
--------------------------------------------------------------------------------
1 | //index.js
2 | //获取应用实例
3 |
4 | const app = getApp()
5 |
6 | Page({})
7 |
--------------------------------------------------------------------------------
/demo/gulp-project-demo/README.md:
--------------------------------------------------------------------------------
1 | # weapp-css-modules的gulp项目示例
2 |
3 | ## 使用
4 |
5 | ```
6 | npm i
7 | npm run build:module
8 |
9 | ```
--------------------------------------------------------------------------------
/demo/gulp-project-demo/src/pages/index/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "usingComponents": {
3 | "child1": "/components/child1/index",
4 | "child2": "/components/child2/index"
5 | }
6 | }
--------------------------------------------------------------------------------
/demo/gulp-project-demo/src/pages/index/index.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | hello,weapp-css-module
6 |
--------------------------------------------------------------------------------
/demo/gulp-project-demo/src/sitemap.json:
--------------------------------------------------------------------------------
1 | {
2 | "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
3 | "rules": [{
4 | "action": "allow",
5 | "page": "*"
6 | }]
7 | }
--------------------------------------------------------------------------------
/demo/gulp-project-demo/src/app.scss:
--------------------------------------------------------------------------------
1 | /**app.wxss**/
2 | .container {
3 | height: 100%;
4 | display: flex;
5 | flex-direction: column;
6 | align-items: center;
7 | justify-content: space-between;
8 | padding: 200rpx 0;
9 | box-sizing: border-box;
10 | }
11 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.1.3
2 |
3 | 调整 gulp 插件包名为 gulp-weapp-css-modules,更新后续适配 webpack 插件计划
4 |
5 | ## 0.1.2
6 |
7 | 适配已编译 css-modules 的场景,新增 generateSimpleScopedName 函数用以生成短类名
8 |
9 | ## 0.1.1
10 |
11 | 修复 needCssModuleTransform 参数未初始化问题
12 |
13 |
14 | ## 0.1.0
15 |
16 | Initial version
17 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "es2021": true,
6 | "node": true
7 | },
8 | "extends": "eslint:recommended",
9 | "parserOptions": {
10 | "ecmaVersion": 12
11 | },
12 | "rules": {
13 | "no-unused-vars": [
14 | "error"
15 | ],
16 | }
17 | }
--------------------------------------------------------------------------------
/demo/gulp-project-demo/src/pages/index/index.scss:
--------------------------------------------------------------------------------
1 | /**index.wxss**/
2 | .userinfo {
3 | display: flex;
4 | flex-direction: column;
5 | align-items: center;
6 | }
7 |
8 | .userinfo-avatar {
9 | width: 128rpx;
10 | height: 128rpx;
11 | margin: 20rpx;
12 | border-radius: 50%;
13 | }
14 |
15 | .userinfo-nickname {
16 | color: #aaa;
17 | }
18 |
19 | .usermotto {
20 | margin-top: 200px;
21 | }
--------------------------------------------------------------------------------
/lib/gulp/remove-map.js:
--------------------------------------------------------------------------------
1 |
2 | const fs = require('fs-extra')
3 | const path = require('path');
4 | const { TEMP_PATH } = require('./utils')
5 |
6 | module.exports = function (toDelList = []) {
7 | const tempJson = fs.readFileSync(path.resolve(__dirname, TEMP_PATH));
8 | Object.values(JSON.parse(tempJson)).forEach(list => {
9 | list.forEach(ele => {
10 | fs.removeSync(ele.source)
11 | })
12 | })
13 | toDelList.forEach(item => {
14 | fs.removeSync(item)
15 | })
16 | fs.removeSync(path.resolve(__dirname, TEMP_PATH))
17 | }
--------------------------------------------------------------------------------
/demo/gulp-project-demo/src/utils/util.js:
--------------------------------------------------------------------------------
1 | const formatTime = date => {
2 | const year = date.getFullYear()
3 | const month = date.getMonth() + 1
4 | const day = date.getDate()
5 | const hour = date.getHours()
6 | const minute = date.getMinutes()
7 | const second = date.getSeconds()
8 |
9 | return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':')
10 | }
11 |
12 | const formatNumber = n => {
13 | n = n.toString()
14 | return n[1] ? n : '0' + n
15 | }
16 |
17 | module.exports = {
18 | formatTime: formatTime
19 | }
20 |
--------------------------------------------------------------------------------
/demo/gulp-project-demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gulp-project-demo",
3 | "version": "1.0.0",
4 | "description": "demo for weapp-css-modules with gulp",
5 | "main": "gulpfile.js",
6 | "scripts": {
7 | "build": "gulp",
8 | "build:module": "gulp build:module"
9 | },
10 | "author": "Silencesnow",
11 | "license": "MIT",
12 | "dependencies": {
13 | "gulp-cli": "^2.3.0"
14 | },
15 | "devDependencies": {
16 | "gulp": "^4.0.2",
17 | "del": "^6.0.0",
18 | "fs-extra": "^9.0.1",
19 | "gulp-if": "^3.0.0",
20 | "gulp-rename": "^2.0.0",
21 | "gulp-sass": "^4.1.0",
22 | "gulp-sort": "^2.0.0",
23 | "gulp-weapp-css-modules": "^0.1.3"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/demo/gulp-project-demo/src/components/child1/index.wxml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/demo/gulp-project-demo/src/components/child2/index.wxml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/demo/gulp-project-demo/src/app.js:
--------------------------------------------------------------------------------
1 | //app.js
2 | App({
3 | onLaunch: function () {
4 | // 展示本地存储能力
5 | var logs = wx.getStorageSync('logs') || []
6 | logs.unshift(Date.now())
7 | wx.setStorageSync('logs', logs)
8 |
9 | // 登录
10 | wx.login({
11 | success: res => {
12 | // 发送 res.code 到后台换取 openId, sessionKey, unionId
13 | }
14 | })
15 | // 获取用户信息
16 | wx.getSetting({
17 | success: res => {
18 | if (res.authSetting['scope.userInfo']) {
19 | // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框
20 | wx.getUserInfo({
21 | success: res => {
22 | // 可以将 res 发送给后台解码出 unionId
23 | this.globalData.userInfo = res.userInfo
24 |
25 | // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回
26 | // 所以此处加入 callback 以防止这种情况
27 | if (this.userInfoReadyCallback) {
28 | this.userInfoReadyCallback(res)
29 | }
30 | }
31 | })
32 | }
33 | }
34 | })
35 | },
36 | globalData: {
37 | userInfo: null
38 | }
39 | })
--------------------------------------------------------------------------------
/demo/gulp-project-demo/src/components/child2/index.module.scss:
--------------------------------------------------------------------------------
1 |
2 | .banner {
3 | position: relative;
4 | overflow: hidden;
5 | height: 100px;
6 | width: 350px;
7 | margin: 50px auto;
8 | &__swiper {
9 | position: absolute;
10 | top: 0;
11 | left: 0;
12 | width: 100%;
13 | height: 100%;
14 | border-radius: 5px;
15 | &-img {
16 | display: block;
17 | width: 100%;
18 | height: 100%;
19 | border-radius: 5px;
20 | }
21 | }
22 |
23 | &__dots {
24 | position: absolute;
25 | left: 0;
26 | right: 0;
27 | bottom: 5px;
28 | height: 4px;
29 | text-align: center;
30 | font-size: 0;
31 | z-index: 1;
32 | }
33 |
34 | &__dot {
35 | display: inline-block;
36 | margin: 0 2px;
37 | width: 4px;
38 | height: 4px;
39 | border-radius: 4px;
40 | opacity: 0.5;
41 | background: #FFF;
42 | transition: all 0.2s;
43 | &--cur {
44 | width: 16px;
45 | background: #FFF;
46 | opacity: 1;
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 AOTU Labs
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/demo/gulp-project-demo/src/components/child1/index.module.wxss:
--------------------------------------------------------------------------------
1 |
2 | .banner {
3 | position: relative;
4 | overflow: hidden;
5 | height: 100px;
6 | width: 350px;
7 | margin: 50px auto;}
8 | .banner__swiper {
9 | position: absolute;
10 | top: 0;
11 | left: 0;
12 | width: 100%;
13 | height: 100%;
14 | border-radius: 5px;}
15 | .banner__swiper-img {
16 | display: block;
17 | width: 100%;
18 | height: 100%;
19 | border-radius: 5px;
20 | }
21 |
22 |
23 | .banner__dots {
24 | position: absolute;
25 | left: 0;
26 | right: 0;
27 | bottom: 5px;
28 | height: 4px;
29 | text-align: center;
30 | font-size: 0;
31 | z-index: 1;
32 | composes: banner;
33 | }
34 |
35 | .banner__dot {
36 | display: inline-block;
37 | margin: 0 2px;
38 | width: 4px;
39 | height: 4px;
40 | border-radius: 4px;
41 | opacity: 0.5;
42 | background: #FFF;
43 | transition: all 0.2s; }
44 | .banner__dot--cur {
45 | width: 16px;
46 | background: #FFF;
47 | opacity: 1;
48 | }
49 |
50 |
51 |
--------------------------------------------------------------------------------
/demo/gulp-project-demo/src/components/child1/index.js:
--------------------------------------------------------------------------------
1 |
2 | import styles from './index.module.wxss';
3 |
4 | Component({
5 | data: {
6 | styles: styles,
7 | current: 0,
8 | list: [
9 | {
10 | "img": "https://img20.360buyimg.com/ling/jfs/t1/134524/9/15501/294288/5fa9f86aEadd02f8a/906f144f4748d16c.jpg",
11 | },
12 | {
13 |
14 | "img": "https://img30.360buyimg.com/ling/jfs/t1/152681/11/5340/265556/5fa9f877E5e0267a6/b0a75f36bf3a1c62.jpg",
15 |
16 | },
17 | {
18 | "img": "https://img13.360buyimg.com/ling/jfs/t1/125393/34/17977/253427/5fa9f870E55e045ce/8150d19f05e323c9.jpg",
19 | },
20 | {
21 | "img": "https://img20.360buyimg.com/ling/jfs/t1/134524/9/15501/294288/5fa9f86aEadd02f8a/906f144f4748d16c.jpg",
22 | },
23 | {
24 | "img": "https://img13.360buyimg.com/ling/jfs/t1/139062/9/13989/165189/5fa9f9b6Ede33f131/15128211fee1f794.jpg",
25 | }]
26 | },
27 | methods: {
28 | onSwiperChange(e) {
29 | const { current } = e.detail;
30 | this.setData({ current })
31 | }
32 | }
33 |
34 | })
--------------------------------------------------------------------------------
/demo/gulp-project-demo/src/components/child2/index.js:
--------------------------------------------------------------------------------
1 |
2 | import styles from './index.module.scss';
3 |
4 | Component({
5 | data: {
6 | styles: styles,
7 | current: 0,
8 | list: [
9 | {
10 | "img": "https://img20.360buyimg.com/ling/jfs/t1/134524/9/15501/294288/5fa9f86aEadd02f8a/906f144f4748d16c.jpg",
11 | },
12 | {
13 |
14 | "img": "https://img30.360buyimg.com/ling/jfs/t1/152681/11/5340/265556/5fa9f877E5e0267a6/b0a75f36bf3a1c62.jpg",
15 |
16 | },
17 | {
18 | "img": "https://img13.360buyimg.com/ling/jfs/t1/125393/34/17977/253427/5fa9f870E55e045ce/8150d19f05e323c9.jpg",
19 | },
20 | {
21 | "img": "https://img20.360buyimg.com/ling/jfs/t1/134524/9/15501/294288/5fa9f86aEadd02f8a/906f144f4748d16c.jpg",
22 | },
23 | {
24 | "img": "https://img13.360buyimg.com/ling/jfs/t1/139062/9/13989/165189/5fa9f9b6Ede33f131/15128211fee1f794.jpg",
25 | }]
26 | },
27 | methods: {
28 | onSwiperChange(e) {
29 | const { current } = e.detail;
30 | this.setData({ current })
31 | }
32 | }
33 |
34 | })
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "weapp-css-modules",
3 | "version": "0.1.3",
4 | "description": "css-modules for weapp in gulp",
5 | "main": "lib/gulp/index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [
10 | "css-modules",
11 | "weapp",
12 | "wxapp"
13 | ],
14 | "author": "Silencesnow",
15 | "license": "MIT",
16 | "dependencies": {
17 | "@babel/core": "^7.12.3",
18 | "@babel/plugin-proposal-decorators": "^7.12.1",
19 | "@babel/plugin-proposal-object-rest-spread": "^7.12.1",
20 | "@babel/plugin-syntax-jsx": "^7.12.1",
21 | "babel-generator": "^6.26.1",
22 | "babel-plugin-transform-class-properties": "^6.24.1",
23 | "babel-traverse": "^6.26.0",
24 | "babel-types": "^6.26.0",
25 | "colors": "^1.4.0",
26 | "fs-extra": "^9.0.1",
27 | "himalaya-walk": "^1.0.0",
28 | "himalaya-wxml": "^1.1.0",
29 | "lodash": "^4.17.20",
30 | "postcss-modules": "^3.2.2",
31 | "postcss-scss": "^3.0.4",
32 | "through2": "^4.0.2",
33 | "gulp-postcss": "^9.0.0"
34 | },
35 | "devDependencies": {
36 | "eslint": "^7.13.0",
37 | "gulp": "^4.0.2",
38 | "gulp-babel": "^8.0.0",
39 | "gulp-if": "^3.0.0",
40 | "gulp-order": "^1.2.0",
41 | "gulp-sort": "^2.0.0"
42 | }
43 | }
--------------------------------------------------------------------------------
/demo/gulp-project-demo/gulpfile.js:
--------------------------------------------------------------------------------
1 | const gulp = require("gulp")
2 | const sass = require('gulp-sass');
3 | const rename = require("gulp-rename");
4 | const del = require('del');
5 | const gulpif = require('gulp-if');
6 | const sort = require('gulp-sort');
7 |
8 | const { weappCssModule, wcmSortFn } = require('gulp-weapp-css-modules')
9 |
10 |
11 |
12 | gulp.task('clean', () => del(['./dist/**/*']));
13 |
14 |
15 | gulp.task('scss', () => {
16 | return gulp.src('./src/**/*.scss')
17 | .pipe(sass().on('error', sass.logError))
18 | .pipe(rename({
19 | extname: ".wxss"
20 | }))
21 | .pipe(gulp.dest('./dist'))
22 | })
23 |
24 | gulp.task('copy', () => {
25 | return gulp.src(['./src/**/*', '!./src/**/*.scss'])
26 | .pipe(gulp.dest('./dist'))
27 | })
28 |
29 | function isScss(file) {
30 | // 判断文件的扩展名是否是 '.scss'
31 | return file.extname === '.scss';
32 | }
33 |
34 | gulp.task('css-module', () => {
35 | return gulp.src('./src/**/*')
36 | .pipe(gulpif(isScss, sass()))
37 | .pipe(sort(wcmSortFn))
38 | .pipe(weappCssModule())
39 | .pipe(gulp.dest('./dist'))
40 | })
41 |
42 | const originBuildSeries = [
43 | 'clean',
44 | 'scss',
45 | 'copy'
46 | ]
47 |
48 | const moduleBuildSeries = [
49 | 'clean',
50 | 'css-module'
51 | ]
52 |
53 | gulp.task('default', gulp.series(originBuildSeries))
54 |
55 | gulp.task('build:module', gulp.series(moduleBuildSeries))
--------------------------------------------------------------------------------
/demo/gulp-project-demo/src/project.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "项目配置文件",
3 | "packOptions": {
4 | "ignore": []
5 | },
6 | "setting": {
7 | "urlCheck": true,
8 | "scopeDataCheck": false,
9 | "coverView": true,
10 | "es6": true,
11 | "postcss": true,
12 | "compileHotReLoad": false,
13 | "preloadBackgroundData": false,
14 | "minified": true,
15 | "autoAudits": false,
16 | "newFeature": true,
17 | "uglifyFileName": false,
18 | "uploadWithSourceMap": true,
19 | "useIsolateContext": true,
20 | "nodeModules": false,
21 | "enhance": false,
22 | "useCompilerModule": false,
23 | "userConfirmedUseCompilerModuleSwitch": false,
24 | "showShadowRootInWxmlPanel": true,
25 | "checkInvalidKey": true,
26 | "checkSiteMap": true,
27 | "babelSetting": {
28 | "ignore": [],
29 | "disablePlugins": [],
30 | "outputPath": ""
31 | }
32 | },
33 | "compileType": "miniprogram",
34 | "libVersion": "2.12.1",
35 | "appid": "",
36 | "projectname": "gulp-project-demo",
37 | "debugOptions": {
38 | "hidedInDevtools": []
39 | },
40 | "isGameTourist": false,
41 | "simulatorType": "wechat",
42 | "simulatorPluginLibVersion": {},
43 | "condition": {
44 | "search": {
45 | "current": -1,
46 | "list": []
47 | },
48 | "conversation": {
49 | "current": -1,
50 | "list": []
51 | },
52 | "game": {
53 | "currentL": -1,
54 | "list": []
55 | },
56 | "miniprogram": {
57 | "current": -1,
58 | "list": []
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/lib/gulp/postcss-deal.js:
--------------------------------------------------------------------------------
1 |
2 | const modules = require('postcss-modules');
3 | const postcss = require('gulp-postcss');
4 | const fs = require('fs-extra')
5 | const colors = require('colors');
6 | const path = require('path')
7 |
8 | const { generateSimpleScopedName } = require('./utils')
9 |
10 | function getJSONFromCssModules(cssFileName, json) {
11 |
12 | fs.writeFileSync(path.join(path.dirname(cssFileName), 'index.module.map.js'), 'module.exports=' + JSON.stringify(json));
13 | }
14 |
15 | const wrapper = postcss([
16 | modules({
17 | getJSON: getJSONFromCssModules,
18 | generateScopedName: generateSimpleScopedName
19 | }),
20 | ])
21 |
22 | module.exports = (file, _, cb) => {
23 |
24 | console.log(colors.green('postcss-modules预处理:', file.relative))
25 |
26 | if (/\.module(\.css$|\.wxss$)/g.test(file.relative)) { // 只处理module标示的部分
27 |
28 | wrapper._transform(file, _, (err, file) => {
29 | const toDel = path.join(path.dirname(file.path), 'index.module.map.js')
30 | file.path = file.path.replace(/\.module(\.css$|\.wxss$)/, '.wxss')
31 |
32 | cb(err, file, toDel)
33 | });
34 | return;
35 |
36 | } else if (/\.js$/g.test(file.relative)) { // 处理引入的路径变更
37 | const content = file.contents.toString();
38 | file.contents = Buffer.from(content.replace(/\.\/index\.module(\.scss|\.wxss|\.less|\.styl)/, './index.module.map'))
39 | }
40 | cb(null, file);
41 | }
--------------------------------------------------------------------------------
/lib/gulp/himalayaStringifyChanged/stringify.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.formatAttributes = formatAttributes;
7 | exports.toHTML = toHTML;
8 |
9 | var _compat = require('./compat');
10 |
11 | function formatAttributes(attributes) {
12 | return attributes.reduce(function (attrs, attribute) {
13 | var key = attribute.key,
14 | value = attribute.value;
15 |
16 | if (value === null) {
17 | return attrs + ' ' + key;
18 | }
19 | var quoteEscape = value.indexOf('"') !== -1;
20 | var quote = quoteEscape ? '\'' : '"';
21 | return attrs + ' ' + key + '=' + quote + value + quote;
22 | }, '');
23 | }
24 |
25 | function toHTML(tree, options) {
26 | return tree.map(function (node) {
27 | if (node.type === 'text') {
28 | return node.content;
29 | }
30 | if (node.type === 'comment') {
31 | return '';
32 | }
33 | var tagName = node.tagName,
34 | attributes = node.attributes,
35 | children = node.children;
36 |
37 | var isSelfClosing = (0, _compat.arrayIncludes)(options.voidTags, tagName.toLowerCase());
38 | return isSelfClosing ? '<' + tagName + formatAttributes(attributes) + '>' : '<' + tagName + formatAttributes(attributes) + '>' + toHTML(children, options) + '' + tagName + '>';
39 | }).join('');
40 | }
41 |
42 | exports.default = { toHTML: toHTML };
43 | //# sourceMappingURL=stringify.js.map
44 |
--------------------------------------------------------------------------------
/lib/gulp/index.js:
--------------------------------------------------------------------------------
1 |
2 | const through2 = require('through2')
3 | const postcssDeal = require('./postcss-deal');
4 | const replaceJs = require('./replace-js')
5 | const replaceWxml = require('./replace-wxml')
6 | const removeMap = require('./remove-map')
7 | const { checkCssFileExistsSync, wcmSortFn, generateSimpleScopedName } = require('./utils')
8 |
9 |
10 | // 主逻辑
11 | function deal(cb, _, error, file, toDelItem) {
12 | if (toDelItem) {
13 | cacheList.push(toDelItem); // 待删除的文件
14 | }
15 |
16 | if (/\.js$/g.test(file.relative)) {
17 | replaceJs(file, _, cb)
18 | return;
19 |
20 | } else if (/\.wxml$/g.test(file.relative)) {
21 | replaceWxml(file, _, cb)
22 | return;
23 | }
24 |
25 | cb(error, file)
26 | }
27 |
28 | const cacheList = []
29 |
30 | const weappCssModule = ({ needCssModuleTransform = true } = {}) => {
31 |
32 | return through2.obj((file, _, cb) => {
33 | // 只对文件夹内包含index.module.样式文件的进行处理
34 | if (checkCssFileExistsSync(file.dirname)) {
35 | if (/(\.js)$|(\.css)$|(\.wxss)$/g.test(file.relative) && needCssModuleTransform) {
36 | postcssDeal(file, _, (...param) => deal(cb, _, ...param)) // 实现css-module的初始逻辑,回调进入主逻辑
37 | } else {
38 | deal(cb, _, null, file) // deal是主逻辑部分
39 | }
40 | return;
41 | }
42 |
43 | cb(null, file)
44 | }, (cb) => {
45 | removeMap(cacheList)
46 | cb()
47 | })
48 | }
49 |
50 | module.exports = {
51 | weappCssModule,
52 | wcmSortFn,
53 | generateSimpleScopedName
54 | }
55 |
56 |
--------------------------------------------------------------------------------
/lib/gulp/himalayaStringifyChanged/compat.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.startsWith = startsWith;
7 | exports.endsWith = endsWith;
8 | exports.stringIncludes = stringIncludes;
9 | exports.isRealNaN = isRealNaN;
10 | exports.arrayIncludes = arrayIncludes;
11 | /*
12 | We don't want to include babel-polyfill in our project.
13 | - Library authors should be using babel-runtime for non-global polyfilling
14 | - Adding babel-polyfill/-runtime increases bundle size significantly
15 |
16 | We will include our polyfill instance methods as regular functions.
17 | */
18 |
19 | function startsWith(str, searchString, position) {
20 | return str.substr(position || 0, searchString.length) === searchString;
21 | }
22 |
23 | function endsWith(str, searchString, position) {
24 | var index = (position || str.length) - searchString.length;
25 | var lastIndex = str.lastIndexOf(searchString, index);
26 | return lastIndex !== -1 && lastIndex === index;
27 | }
28 |
29 | function stringIncludes(str, searchString, position) {
30 | return str.indexOf(searchString, position || 0) !== -1;
31 | }
32 |
33 | function isRealNaN(x) {
34 | return typeof x === 'number' && isNaN(x);
35 | }
36 |
37 | function arrayIncludes(array, searchElement, position) {
38 | var len = array.length;
39 | if (len === 0) return false;
40 |
41 | var lookupIndex = position | 0;
42 | var isNaNElement = isRealNaN(searchElement);
43 | var searchIndex = lookupIndex >= 0 ? lookupIndex : len + lookupIndex;
44 | while (searchIndex < len) {
45 | var element = array[searchIndex++];
46 | if (element === searchElement) return true;
47 | if (isNaNElement && isRealNaN(element)) return true;
48 | }
49 |
50 | return false;
51 | }
52 | //# sourceMappingURL=compat.js.map
53 |
--------------------------------------------------------------------------------
/lib/gulp/replace-wxml.js:
--------------------------------------------------------------------------------
1 | const himalayaWxml = require("himalaya-wxml");
2 | const himalayaWalk = require('himalaya-walk');
3 | const colors = require('colors');
4 | const babelCore = require('@babel/core')
5 | const generate = require("babel-generator").default;
6 | const t = require('babel-types')
7 | const himalayaStringify = require("./himalayaStringifyChanged/stringify");
8 | const { getConfigMap } = require('./utils')
9 |
10 | const reg = /[{ ](([\w_-]*)[.[]['"]?([\w_-]*)['"]?\]?)[} )]/g;
11 |
12 | function transverse(obj, array) {
13 | if (t.isBinaryExpression(obj)) {
14 | transverse(obj.left, array);
15 | transverse(obj.right, array);
16 | } else {
17 | if (t.isStringLiteral(obj)) {
18 | obj.value.trim() && array.push(obj.value)
19 | } else {
20 | array.push("{{" + generate(obj).code + "}}")
21 | }
22 | }
23 | }
24 |
25 | module.exports = (file, _, cb) => {
26 | let needReplace = false;
27 | const styleMap = getConfigMap(file.relative)
28 |
29 |
30 | console.log(colors.green('wxml替换开始:', file.relative))
31 |
32 | const code = himalayaWxml.parse(file.contents.toString())
33 | himalayaWalk(code, node => {
34 | if (node.attributes) {
35 | node.attributes.forEach(attribute => {
36 | if (attribute.key === 'class' && attribute.value) {
37 | let result = attribute.value;
38 |
39 | attribute.value.replace(reg, (match, $1, $2, $3) => {
40 | // 形如 {{ style.xx + '' + style.yy }}
41 | if (styleMap[$2]) {// 有对应的map值
42 | needReplace = true;
43 | if (styleMap[$2][$3]) {
44 | result = result.replace($1, "'" + styleMap[$2][$3] + "'");
45 | } else {
46 | console.log(colors.red('wxml替换发现未匹配cssMap的类名:', $3, file.relative))
47 | result = result.replace($1, "'" + $3 + "'");
48 | }
49 | }
50 | })
51 | if (needReplace && /{{.+\}\}/.test(result)) {
52 | const value = babelCore.parse(result).program.body[0].body[0].body[0].expression; // {{}}需要取2层
53 | const array = [];
54 | transverse(value, array);
55 | attribute.value = array.join(' ');
56 | }
57 | }
58 | })
59 | }
60 | })
61 | if (needReplace) {
62 | console.log(colors.green('wxml替换完成:', file.relative))
63 | const ele = himalayaStringify.toHTML(code, himalayaWxml.parseDefaults);
64 | file.contents = Buffer.from(ele);
65 | }
66 | cb(null, file);
67 | }
68 |
--------------------------------------------------------------------------------
/lib/gulp/utils.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra');
2 | const path = require('path');
3 |
4 | const TEMP_PATH = path.resolve(__dirname, './temp.json') // css-map的缓存文件
5 |
6 | function getConfigMap(filenameRelative) {
7 | let data;
8 | // 读取配置文件,判断本文件是否有相应map
9 | try {
10 | data = fs.readFileSync(TEMP_PATH);
11 | } catch (e) {
12 | data = '{}'
13 | }
14 |
15 | const cssMap = JSON.parse(data);
16 | const fileName = filenameRelative.replace(/\.(wxml|wxss)/, '.js')
17 | if (cssMap[fileName]) {
18 | data = getMapByList(cssMap[fileName])
19 | if (filenameRelative.indexOf('.wxss') > -1) {
20 | return data.cssMap
21 | } else {
22 | return data.styleMap
23 | }
24 | }
25 | return {};
26 | }
27 |
28 |
29 | function getMapByList(list) {
30 | let styleMap = {};
31 | let cssMap = {};
32 | list.forEach((ele, index) => {
33 | let data = getMapFromLocation(ele.source, index)
34 | if (data) {
35 | styleMap[ele.name] = data.styleMap;
36 | cssMap = { ...cssMap, ...data.cssMap } // cssMap是个多map合并的对象,暂不考虑类名重叠的问题
37 | }
38 | })
39 | return { styleMap, cssMap }
40 | }
41 |
42 | const cacheMap = {}
43 |
44 | function getMapFromLocation(location) {
45 |
46 | if (!cacheMap[location]) {
47 | const data = require(location);
48 | let styleMap = {};
49 | // Object.keys(data).forEach((name, index) => {
50 | // const shortName = getShortName(index, order)
51 | // // styleMap[name] = shortName; // name : a;
52 | // // cssMap[data[name]] = shortName; // hash : a;
53 | // })
54 | styleMap = data;
55 | cacheMap[location] = {
56 | styleMap,
57 | // cssMap
58 | }
59 |
60 | }
61 | return cacheMap[location]
62 | }
63 |
64 | const str = 'abcdefghijklmnopqrstuvwxyz'
65 | function getShortName(index, order = 0) {
66 | const arr = parseInt(index, 10).toString(26).split('');
67 | let keys = arr.map(item => str[parseInt(item, 26)]).join('')
68 | if (order !== 0) {
69 | keys += order
70 | }
71 | return keys;
72 | }
73 | function checkCssFileExistsSync(filepath) {
74 | let flag = false;
75 | ['scss', 'css', 'less', 'styl', 'wxss'].forEach(item => {
76 | try {
77 | fs.accessSync(path.join(filepath, 'index.module.' + item), fs.constants.F_OK);
78 | flag = true;
79 | } catch (e) { } // eslint-disable-line
80 | })
81 |
82 | return flag;
83 | }
84 |
85 | function getWeight(path) {
86 | if (/(\.wxss)$|(\.scss)$|(\.css)$/g.test(path)) {
87 | return 3;
88 | } else if (/(\.js)$/g.test(path)) {
89 | return 2;
90 | } else if (/(\.wxml)$/g.test(path)) {
91 | return 1;
92 | }
93 | }
94 |
95 | function wcmSortFn(a, b) {
96 | const aValue = getWeight(a.relative);
97 | const bValue = getWeight(b.relative);
98 | return bValue - aValue
99 | }
100 |
101 | let map = {}
102 | function generateSimpleScopedName(name, filename) {
103 | if (undefined === map[filename]) {
104 | map[filename] = {
105 | index: 0,
106 | list: {
107 | [name]: getShortName(0)
108 | }
109 | };
110 | } else if (!map[filename].list[name]) {
111 | let shortName = getShortName(++map[filename].index);
112 | map[filename].list[name] = shortName
113 | }
114 |
115 | return map[filename].list[name];
116 | }
117 |
118 | module.exports = {
119 | getConfigMap,
120 | getMapByList,
121 | getShortName,
122 | checkCssFileExistsSync,
123 | generateSimpleScopedName,
124 | wcmSortFn,
125 | TEMP_PATH
126 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # weapp-css-modules
2 |
3 | 小程序的简化版 css-modules,比标准 [css-modules](https://github.com/css-modules/css-modules) 代码量更少的优化方案
4 |
5 | ## 介绍
6 |
7 | css-modules 是一种 css 模块化方案,它在构建过程中生成一个原类名与新类名的 map,根据 map引用样式,通过设定 hash 规则,实现了对 CSS 类名作用域的限定,它通常用来解决页面类名冲突的问题。由于微信小程序内组件样式默认隔离,为什么要使用 css-modules 呢?
8 |
9 | 有以下2个原因:
10 |
11 | - hash 化后可以实现更短的命名,减少代码包体积
12 | - 跨端项目需要兼顾非小程序环境,避免样式冲突
13 |
14 | weapp-css-modules 做了哪些事?
15 |
16 | - 新类名单字母编排,减少代码量
17 | - 移除类名映射 map,替换 js 和 wxml 中变量为编译后类名
18 |
19 | 标准 css-modules 方案:
20 |
21 | ```
22 | import style from './index.wxss'
23 |
24 | .index_banner_xkpkl { xx }
25 | module.exports ={'banner' : 'index_banner_xkpkl'} // 额外生成的 map 文件
26 | ```
27 | weapp-css-modules 编译后效果:
28 | ```
29 | let style = {}
30 |
31 | .a { xx }
32 | ```
33 |
34 | ## 安装
35 | 目前只开发了适用于使用 gulp 编译小程序的 gulp 插件,后续计划开发 webpack 可用的插件实现相同功能
36 |
37 | ```
38 | npm i gulp-weapp-css-modules gulp-sort
39 | ```
40 |
41 | ```
42 | // gulpfile.js
43 | const { weappCssModule, wcmSortFn } = require('gulp-weapp-css-modules')
44 | const sort = require('gulp-sort');
45 |
46 | gulp.task('css-module', () => {
47 | return gulp.src('./src/**/*')
48 | .pipe(sort(wcmSortFn)) // 由于处理文件有顺序依赖,需要先对文件排序
49 | .pipe(weappCssModule())
50 | .pipe(gulp.dest('./dist'))
51 | })
52 | ```
53 |
54 | ## 使用
55 |
56 | 小程序页面不具备隔离功能,因此只有具备样式隔离的 Component 可以改造使用 weapp-css-modules
57 |
58 | 1. css 文件改名字: weapp-css-modules 通过 css 文件是否带 module 来识别需要替换的内容
59 |
60 | `index.wxss` -> `index.module.wxss`
61 |
62 | // 或者使用scss/其他
63 |
64 | `index.scss` -> `index.module.scss`
65 |
66 | 2. js 内新增样式文件的引入,目的是建立 css-modules 的样式与 js 关系
67 | ```
68 | import styles from './index.module.wxss
69 |
70 | data:{
71 | ...,
72 | styles:styles
73 | }
74 |
75 | ```
76 |
77 | 3. 修改 js 内类名的地方替换为 styles 的间接引入
78 | ```
79 | query.select('.banner')
80 | .boundingClientRect()
81 | .exec(function (res) {...})
82 |
83 | // 改为
84 | query.select('.' + styles['banner'])
85 | .boundingClientRect()
86 | .exec(function (res) {...})
87 |
88 | ```
89 |
90 | 4. 修改 wxml 内类名的使用
91 |
92 | 4.1. 普通类名
93 | ```
94 |
95 | // 改为
96 |
97 | // 或者
98 |
99 | ```
100 | 4.2. 三目运算符
101 | ```
102 |
103 |
104 | // 改为
105 |
106 | // 或者
107 |
108 | ```
109 |
110 | 这里需要注意几种有问题的写法:
111 |
112 | 4.2.1. 类名间未加空格
113 |
114 | ```
115 |
116 | ```
117 | 4.2.2. 三目表达式未加括号,运算优先级不明
118 |
119 | ```
120 |
121 | ```
122 | 4.2.3. styles 的属性需要是具体的字符串,不能使用变量表达式(这是 weapp-css-modules 需要单独关注的地方,因为编译阶段会对 styles.xx 进行求值,所以不能把表达式写在属性位置)
123 | ```
124 |
125 | ```
126 | 5. 构建过程中关注脚本的红色提示,类似于这种:
127 | 
128 |
129 | 这是由于在 js/wxml 内使用了一个`banner__swiper_2`,而 css 内并没有定义`banner__swiper_2`,css-module 编译的 map 文件是根据 css 内的样式定义来生成 key 名的,因此`styles['banner__swiper_2']`是`undefined`, 针对这种情况有两种处理方式:
130 |
131 | 5.1. 如果 js 内需要通过这个类名选择到某个元素,但是 css 内不需要编写样式,那么可以将它视为不需要编译的类名,即:
132 | ```
133 | query.selector('.banner__swiper_2') // 不改成 styles.xx 的写法
134 | // 相应的元素也不索引到 styles
135 | // 这样实现了一个组件内不会被编译的样式
136 | ```
137 | 5.2. 如果 js 内无引用,那么删掉 wxml 内该类名的定义吧~
138 |
139 | 6. 构建完进行检查,关注样式和交互是否正常
140 |
141 | ## 参考示例
142 |
143 | - gulp项目:路径 `/demo/gulp-project-demo`
144 |
145 | ## 联系反馈
146 |
147 | * 欢迎通过邮箱来跟我联系: smile123ing@163.com
148 | * 欢迎通过 [GitHub issue](https://github.com/o2team/weapp-css-modules/issues) 提交 BUG、以及其他问题
149 | * 欢迎给该项目点个赞 ⭐️ [star on GitHub](https://github.com/o2team/weapp-css-modules) !
--------------------------------------------------------------------------------
/lib/gulp/replace-js.js:
--------------------------------------------------------------------------------
1 | const { get } = require('lodash')
2 | const colors = require('colors');
3 | const fs = require('fs');
4 | const path = require('path');
5 | const t = require('babel-types')
6 | const babelCore = require('@babel/core')
7 |
8 | const { TEMP_PATH, getMapByList } = require('./utils')
9 |
10 | module.exports = (file, _, cb) => {
11 |
12 | const fileOpts = Object.assign({}, {
13 | plugins: [
14 | '@babel/plugin-syntax-jsx',
15 | ["@babel/plugin-proposal-decorators", { "legacy": true }],
16 | 'transform-class-properties',
17 | '@babel/plugin-proposal-object-rest-spread',
18 | replaceJs
19 | ]
20 | }, {
21 | filename: file.path,
22 | filenameRelative: file.relative,
23 | sourceMap: Boolean(file.sourceMap),
24 | sourceFileName: file.relative,
25 | caller: { name: 'babel-gulp' }
26 | });
27 |
28 | babelCore.transformAsync(file.contents.toString(), fileOpts).then(res => {
29 | if (res) {
30 | file.contents = Buffer.from(res.code);
31 | }
32 | }).catch(err => {
33 | this.emit('error', err);
34 | }).then(
35 | () => cb(null, file),
36 | () => cb(null, file)
37 | );
38 | }
39 |
40 | const nestedVisitor = {
41 | ImportDeclaration(_path) {
42 | if (/\.module\.map/g.test(get(_path, 'node.source.value', ''))) { // 通过这种方式识别出样式文件的引入
43 | const name = get(_path, 'node.specifiers[0].local.name');
44 | this.list.push({
45 | source: path.join(path.dirname(this.state.filename), _path.node.source.value),
46 | name: name,
47 | })
48 | _path.insertAfter(t.variableDeclaration('let', [t.variableDeclarator(t.identifier(name), t.objectExpression([]))]));// 新增一句let style ={}; 用以容错未使用的一些style
49 | _path.remove()
50 | }
51 | }
52 | }
53 |
54 | function replaceJs({ types: t }) {
55 | return {
56 | pre() {
57 | this.list = [];
58 | this.styleList = [];
59 | this.styleMap = {}
60 | },
61 |
62 | visitor: {
63 | Program(path, state) {
64 | path.traverse(nestedVisitor, { state, list: this.list });
65 | if (this.list.length) {
66 | console.log(colors.green('js替换开始:', state.file.opts.filenameRelative))
67 | this.styleMap = getMapByList(this.list).styleMap
68 | this.styleList = Object.keys(this.styleMap);
69 | }
70 | },
71 | Identifier(_path, state) { // 标识符替换
72 | if (_path.isReferencedIdentifier()) {
73 | if (this.styleList.includes(_path.node.name)) { // 有对应的styleName
74 | const styleName = _path.node.name;
75 | if (t.isMemberExpression(_path.parent)) { // 有使用对应属性
76 | let name;
77 |
78 | if (t.isIdentifier(_path.parent.property)) { // 形如 style.xx
79 | name = _path.parent.property.name;
80 | } else if (t.isStringLiteral(_path.parent.property)) { // 形如 style[xx]
81 | name = _path.parent.property.value
82 | } else {
83 | console.warn('未知的属性', _path.parent)
84 | }
85 |
86 | if (this.styleMap[styleName][name]) {// 有对应的map值
87 | _path.parentPath.replaceWith(t.StringLiteral(this.styleMap[styleName][name]))
88 |
89 | } else if (this.styleMap[styleName]) {
90 | console.log(colors.red('js替换发现未匹配cssMap的类名:', name, state.opts.filenameRelative))
91 | _path.parentPath.replaceWith(t.StringLiteral(name))
92 | }
93 | } else {
94 | _path.replaceWith(t.ObjectExpression([])) // 替换为空表达式
95 | }
96 | }
97 |
98 | }
99 | }
100 |
101 | },
102 | post(state) {
103 | if (this.list.length) {
104 | const { filenameRelative } = state.opts;
105 | console.log(colors.green('js替换完成,生成css配置:', filenameRelative))
106 |
107 | let fileMap;
108 | try {
109 | fileMap = JSON.parse(fs.readFileSync(TEMP_PATH, 'utf-8'))
110 | } catch (e) {
111 | fileMap = {}
112 | }
113 | const temp = {};
114 | temp[filenameRelative] = this.list;
115 |
116 | fs.writeFileSync(TEMP_PATH, JSON.stringify({
117 | ...fileMap,
118 | ...temp
119 | }))
120 | }
121 | }
122 | }
123 | }
--------------------------------------------------------------------------------