Skip to content

Commit 8ef9076

Browse files
authored
Merge pull request #2027 from didi/fix/importStyleStripCondition
feat: support import style strip condition
2 parents 1075f74 + 7c2d626 commit 8ef9076

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+807
-232
lines changed

.eslintrc.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ module.exports = {
3131
camelcase: 0,
3232
indent: 0,
3333
'symbol-description': 0,
34-
'node/no-callback-literal': 0
34+
'node/no-callback-literal': 0,
35+
'space-before-function-paren': 0
3536
},
3637
env: {
3738
'jest/globals': true,

jest.config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@
1212
"testPathIgnorePatterns": ["dist", "node_modules"],
1313
"projects": [
1414
"<rootDir>/packages/*/jest.config.json"
15-
]
15+
],
16+
"testTimeout": 10000
1617
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"lint": "eslint --ext .js,.ts,.tsx,.jsx packages/",
88
"fix": "eslint --fix --ext .js,.ts,.tsx,.jsx packages/",
99
"lint:js": "eslint --ext .js packages/",
10-
"test": "jest",
10+
"test": "jest --passWithNoTests",
1111
"release": "npm run lint && npm run test && npx lerna version",
1212
"docs:dev": "vitepress dev docs-vitepress",
1313
"docs:build": "vitepress build docs-vitepress",

packages/webpack-plugin/jest.config.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
"testEnvironment": "jsdom",
66
"moduleNameMapper": {
77
"\\.(css|styl)$": "identity-obj-proxy"
8-
}
9-
}
8+
},
9+
"testTimeout": 10000
10+
}

packages/webpack-plugin/lib/helpers.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ module.exports = function createHelpers (loaderContext) {
6969
if (part.useJSONJS) options.useJSONJS = true
7070
// eslint-disable-next-line no-fallthrough
7171
case 'styles':
72+
options.lang = part.lang
73+
// eslint-disable-next-line no-fallthrough
7274
case 'template':
7375
options.extract = true
7476
}

packages/webpack-plugin/lib/index.js

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1907,24 +1907,41 @@ try {
19071907
normalModuleFactory.hooks.afterResolve.tap('MpxWebpackPlugin', ({ createData }) => {
19081908
const { queryObj } = parseRequest(createData.request)
19091909
const loaders = createData.loaders
1910+
1911+
// 样式 loader 类型检测和条件编译 loader 插入的工具函数
1912+
const STYLE_LOADER_TYPES = ['stylus-loader', 'sass-loader', 'less-loader', 'css-loader', wxssLoaderPath]
1913+
const injectStyleStripLoader = (loaders) => {
1914+
// 检查是否已经存在 stripLoader
1915+
const hasStripLoader = loaders.some(loader => {
1916+
const loaderPath = toPosix(loader.loader)
1917+
return loaderPath.includes('style-compiler/strip-conditional-loader')
1918+
})
1919+
if (hasStripLoader) {
1920+
return
1921+
}
1922+
const loaderTypes = new Map(STYLE_LOADER_TYPES.map(type => [`node_modules/${type}`, -1]))
1923+
loaders.forEach((loader, index) => {
1924+
const currentLoader = toPosix(loader.loader)
1925+
for (const [key] of loaderTypes) {
1926+
if (currentLoader.includes(key)) {
1927+
loaderTypes.set(key, index)
1928+
break
1929+
}
1930+
}
1931+
})
1932+
const targetIndex = STYLE_LOADER_TYPES
1933+
.map(type => loaderTypes.get(`node_modules/${type}`))
1934+
.find(index => index !== -1)
1935+
1936+
if (targetIndex !== undefined) {
1937+
loaders.splice(targetIndex + 1, 0, { loader: styleStripConditionalPath })
1938+
}
1939+
}
19101940
if (queryObj.mpx && queryObj.mpx !== MPX_PROCESSED_FLAG) {
19111941
const type = queryObj.type
19121942
const extract = queryObj.extract
1913-
19141943
if (type === 'styles') {
1915-
let insertBeforeIndex = -1
1916-
// 单次遍历收集所有索引
1917-
loaders.forEach((loader, index) => {
1918-
const currentLoader = toPosix(loader.loader)
1919-
if (currentLoader.includes('node_modules/stylus-loader') || currentLoader.includes('node_modules/sass-loader') || currentLoader.includes('node_modules/less-loader')) {
1920-
insertBeforeIndex = index
1921-
}
1922-
})
1923-
1924-
if (insertBeforeIndex !== -1) {
1925-
loaders.splice(insertBeforeIndex, 0, { loader: styleStripConditionalPath })
1926-
}
1927-
loaders.push({ loader: styleStripConditionalPath })
1944+
injectStyleStripLoader(loaders)
19281945
}
19291946

19301947
switch (type) {
@@ -1978,6 +1995,7 @@ try {
19781995
}
19791996
// mpxStyleOptions 为 mpx style 文件的标识,避免 Vue 文件插入 styleCompiler 后导致 vue scoped 样式隔离失效
19801997
if (isWeb(mpx.mode) && queryObj.mpxStyleOptions) {
1998+
injectStyleStripLoader(loaders)
19811999
const firstLoader = loaders[0] ? toPosix(loaders[0].loader) : ''
19822000
const isPitcherRequest = firstLoader.includes('node_modules/vue-loader/lib/loaders/pitcher')
19832001
let cssLoaderIndex = -1

packages/webpack-plugin/lib/style-compiler/strip-conditional-loader.js

Lines changed: 152 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
const fs = require('fs/promises')
2+
const parseRequest = require('../utils/parse-request')
3+
const atImport = require('postcss-import')
4+
const { default: postcss } = require('postcss')
5+
16
class Node {
2-
constructor (type, condition = null) {
7+
constructor(type, condition = null) {
38
this.type = type // 'If', 'ElseIf', 'Else' 或 'Text'
49
this.condition = condition // If 或 Elif 的条件
510
this.children = []
@@ -8,7 +13,7 @@ class Node {
813
}
914

1015
// 提取 css string 为 token
11-
function tokenize (cssString) {
16+
function tokenize(cssString) {
1217
const regex = /\/\*\s*@mpx-(if|elif|else|endif)(?:\s*\((.*?)\))?\s*\*\//g
1318
const tokens = []
1419
let lastIndex = 0
@@ -37,12 +42,12 @@ function tokenize (cssString) {
3742
}
3843

3944
// parse:将生成的 token 数组构造成嵌套的 AST
40-
function parse (cssString) {
45+
function parse(cssString) {
4146
const tokens = tokenize(cssString)
4247
const ast = []
4348
const nodeStack = []
4449
let currentChildren = ast
45-
tokens.forEach(token => {
50+
tokens.forEach((token) => {
4651
if (token.type === 'text') {
4752
const node = new Node('Text')
4853
node.value = token.content
@@ -77,10 +82,10 @@ function parse (cssString) {
7782
return ast
7883
}
7984

80-
function evaluateCondition (condition, defs) {
85+
function evaluateCondition(condition, defs) {
8186
try {
8287
const keys = Object.keys(defs)
83-
const values = keys.map(key => defs[key])
88+
const values = keys.map((key) => defs[key])
8489
/* eslint-disable no-new-func */
8590
const func = new Function(...keys, `return (${condition});`)
8691
return func(...values)
@@ -90,10 +95,10 @@ function evaluateCondition (condition, defs) {
9095
}
9196
}
9297

93-
function traverseAndEvaluate (ast, defs) {
98+
function traverseAndEvaluate(ast, defs) {
9499
let output = ''
95100
let batchedIf = false
96-
function traverse (nodes) {
101+
function traverse(nodes) {
97102
for (const node of nodes) {
98103
if (node.type === 'Text') {
99104
output += node.value
@@ -118,10 +123,145 @@ function traverseAndEvaluate (ast, defs) {
118123
return output
119124
}
120125

121-
module.exports = function (css) {
126+
/**
127+
*
128+
* @param {string} content
129+
* @param {Record<string, any>} defs
130+
* @returns
131+
*/
132+
function stripCondition(content, defs) {
133+
const ast = parse(content)
134+
const result = traverseAndEvaluate(ast, defs)
135+
return result
136+
}
137+
138+
/**
139+
* @typedef {Object} StripByPostcssOption
140+
* @property {string} lang 样式语法格式
141+
* @property {string} resourcePath 文件路径
142+
* @property {string} css 源文件
143+
* @property {Record<string, any>} defs 条件定义
144+
* @property {import('webpack').LoaderContext<any>['resolve']} resolve webpack resolve 方法
145+
*/
146+
147+
/**
148+
* @typedef {Object} AtImportConfig
149+
* @property {string} from 当前文件路径
150+
* @property {(filename: string) => Promise<string> | string;} load 加载文件内容的函数
151+
* @property {(id: string, base: string) => Promise<string | null> | string | null;} resolve 解析文件路径的函数
152+
*/
153+
154+
/**
155+
*
156+
* @param {Function} callback
157+
* @param {string} name
158+
* @returns
159+
*/
160+
const shouldInstallWarning = (callback, name) => {
161+
return () => {
162+
try {
163+
return callback()
164+
} catch (error) {
165+
throw new Error(
166+
`[mpx-strip-conditional-loader]: ${name} is not installed, please install it first.\norginal Error: ${
167+
error?.message ?? error.toString()
168+
}`,
169+
{
170+
cause: error
171+
}
172+
)
173+
}
174+
}
175+
}
176+
/**
177+
*
178+
* @typedef {import('postcss').ProcessOptions} ProcessOptions
179+
* @typedef {import('postcss').Root} Root
180+
*
181+
* @type {Record<string, ProcessOptions['syntax']>}
182+
*/
183+
const styleSyntaxProcesserMap = {
184+
stylus: shouldInstallWarning(() => require('postcss-styl'), 'postcss-styl'),
185+
less: shouldInstallWarning(() => require('postcss-less'), 'postcss-less'),
186+
scss: shouldInstallWarning(() => require('postcss-scss'), 'postcss-scss')
187+
}
188+
189+
/**
190+
* @param {StripByPostcssOption} options
191+
*/
192+
async function stripByPostcss(options) {
193+
const syntax = styleSyntaxProcesserMap[options.lang]?.()
194+
const defs = options.defs ?? {}
195+
196+
const afterConditionStrip = stripCondition(options.css, defs)
197+
198+
/**
199+
* @type {import('postcss').AcceptedPlugin[]}
200+
*/
201+
const plugins = [
202+
atImport({
203+
async load(filename) {
204+
let content = await fs.readFile(filename, 'utf-8')
205+
const processer = postcss(plugins)
206+
207+
content = stripCondition(content, defs)
208+
209+
const { css } = await processer.process(content, {
210+
syntax,
211+
from: filename,
212+
to: options.resourcePath
213+
})
214+
return css
215+
},
216+
resolve: (id, base) => {
217+
return new Promise((resolve, reject) => {
218+
options.resolve(base, id, (err, res) => {
219+
if (err) return reject(err)
220+
if (typeof res !== 'string') {
221+
return reject(
222+
new Error(
223+
`[mpx-strip-conditional-loader]: Cannot resolve ${id} from ${base}`
224+
)
225+
)
226+
}
227+
resolve(res)
228+
})
229+
})
230+
}
231+
})
232+
]
233+
234+
const processer = postcss(plugins)
235+
236+
return processer.process(afterConditionStrip, {
237+
from: options.resourcePath,
238+
syntax
239+
})
240+
}
241+
242+
/**
243+
*
244+
* @this {import('webpack').LoaderContext<any>}
245+
* @param {string} css
246+
*/
247+
module.exports = async function (css) {
122248
this.cacheable()
249+
250+
const callback = this.async()
251+
123252
const mpx = this.getMpx()
124-
const defs = mpx.defs
125-
const ast = parse(css)
126-
return traverseAndEvaluate(ast, defs)
253+
const { resourcePath, queryObj } = parseRequest(this.resource)
254+
255+
const result = await stripByPostcss({
256+
lang: queryObj.lang,
257+
resourcePath,
258+
css,
259+
defs: mpx.defs,
260+
resolve: this.resolve.bind(this)
261+
})
262+
263+
callback(null, result.css, result.map)
127264
}
265+
266+
module.exports.stripByPostcss = stripByPostcss
267+
module.exports.stripCondition = stripCondition

packages/webpack-plugin/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"mime": "^2.2.2",
5252
"object-assign": "^4.1.1",
5353
"postcss": "^8.4.5",
54+
"postcss-import": "^16.1.1",
5455
"postcss-load-config": "^3.1.1",
5556
"postcss-modules-extract-imports": "^3.0.0",
5657
"postcss-modules-local-by-default": "^4.0.0",
@@ -63,6 +64,9 @@
6364
"webpack-virtual-modules": "^0.6.0"
6465
},
6566
"peerDependencies": {
67+
"postcss-less": "^6.0.0",
68+
"postcss-scss": "^4.0.9",
69+
"postcss-styl": "^0.12.3",
6670
"webpack": "^5.75.0"
6771
},
6872
"publishConfig": {
@@ -78,7 +82,7 @@
7882
"url": "https://github.com/didi/mpx/issues"
7983
},
8084
"scripts": {
81-
"test": "jest",
85+
"test": "jest --passWithNoTests",
8286
"copy-icons": "cp -r ./lib/runtime/components/react/mpx-icon/icons ./lib/runtime/components/react/dist/mpx-icon/icons",
8387
"build": "rimraf ./lib/runtime/components/react/dist && tsc && npm run copy-icons"
8488
},
@@ -87,7 +91,10 @@
8791
"@mpxjs/api-proxy": "^2.10.16",
8892
"@types/babel-traverse": "^6.25.4",
8993
"@types/babel-types": "^7.0.4",
94+
"@types/glob": "^8.1.0",
95+
"@types/postcss-import": "^14.0.3",
9096
"@types/react": "^18.2.79",
97+
"glob": "^11.0.2",
9198
"react-native": "^0.74.5",
9299
"react-native-gesture-handler": "^2.18.1",
93100
"react-native-linear-gradient": "^2.8.3",

0 commit comments

Comments
 (0)