Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/webpack-plugin/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ const LoadAsyncChunkModule = require('./react/LoadAsyncChunkModule')
const ExternalModule = require('webpack/lib/ExternalModule')
const { RetryRuntimeModule, RetryRuntimeGlobal } = require('./dependencies/RetryRuntimeModule')
const checkVersionCompatibility = require('./utils/check-core-version-match')

const { rewriteFSForCss, startFSStripForCss } = require('./style-compiler/strip-conditional-loader')
rewriteFSForCss()
checkVersionCompatibility()

const isProductionLikeMode = options => {
Expand Down Expand Up @@ -323,6 +324,9 @@ class MpxWebpackPlugin {
}

apply (compiler) {
// 注入 fs 代理
startFSStripForCss(this.options.defs)

if (!compiler.__mpx__) {
compiler.__mpx__ = true
} else {
Expand Down
276 changes: 56 additions & 220 deletions packages/webpack-plugin/lib/style-compiler/strip-conditional-loader.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
const fs = require('fs/promises')
const parseRequest = require('../utils/parse-request')
const path = require('path')
const loaderUtils = require('loader-utils')
const fs = require('fs')

class Node {
constructor(type, condition = null) {
Expand Down Expand Up @@ -29,8 +26,7 @@ function tokenize(cssString) {
// match[2] 为条件(如果存在)
tokens.push({
type: match[1], // 'if'、'elif'、'else' 或 'endif'
condition: match[2] ? match[2].trim() : null,
rawValue: match[0]
condition: match[2] ? match[2].trim() : null
})
lastIndex = regex.lastIndex
}
Expand All @@ -55,7 +51,6 @@ function parse(cssString) {
currentChildren.push(node)
} else if (token.type === 'if') {
const node = new Node('If', token.condition)
node.rawValue = token.rawValue || ''
currentChildren.push(node)
nodeStack.push(currentChildren)
currentChildren = node.children
Expand All @@ -65,7 +60,6 @@ function parse(cssString) {
}
currentChildren = nodeStack[nodeStack.length - 1]
const node = new Node('ElseIf', token.condition)
node.rawValue = token.rawValue || ''
currentChildren.push(node)
currentChildren = node.children
} else if (token.type === 'else') {
Expand All @@ -74,16 +68,12 @@ function parse(cssString) {
}
currentChildren = nodeStack[nodeStack.length - 1]
const node = new Node('Else')
node.rawValue = token.rawValue || ''
currentChildren.push(node)
currentChildren = node.children
} else if (token.type === 'endif') {
const node = new Node('EndIf')
node.rawValue = token.rawValue || ''
if (nodeStack.length > 0) {
currentChildren = nodeStack.pop()
}
currentChildren.push(node)
}
})
return ast
Expand Down Expand Up @@ -112,22 +102,17 @@ function traverseAndEvaluate(ast, defs) {
} else if (node.type === 'If') {
// 直接判断 If 节点
batchedIf = false
output += node.rawValue || ''
if (evaluateCondition(node.condition, defs)) {
traverse(node.children)
batchedIf = true
}
} else if (node.type === 'ElseIf' && !batchedIf) {
output += node.rawValue || ''
if (evaluateCondition(node.condition, defs)) {
traverse(node.children)
batchedIf = true
}
} else if (node.type === 'Else' && !batchedIf) {
output += node.rawValue || ''
traverse(node.children)
} else if (node.type === 'EndIf') {
output += node.rawValue || ''
}
}
}
Expand All @@ -143,212 +128,74 @@ function traverseAndEvaluate(ast, defs) {
*/
function stripCondition(content, defs) {
const ast = parse(content)
const result = traverseAndEvaluate(ast, defs)
return result
return traverseAndEvaluate(ast, defs)
}

/**
* @typedef {Object} StripByPostcssOption
* @property {string} lang 样式语法格式
* @property {string} resourcePath 文件路径
* @property {string} css 源文件
* @property {Record<string, any>} defs 条件定义
* @property {import('webpack').LoaderContext<any>['resolve']} resolve webpack resolve 方法
*/

/**
* @typedef {Object} AtImportConfig
* @property {string} from 当前文件路径
* @property {(filename: string) => Promise<string> | string;} load 加载文件内容的函数
* @property {(id: string, base: string) => Promise<string | null> | string | null;} resolve 解析文件路径的函数
*/
let proxyReadFileSync
let proxyReadFile
const rawReadFileSync = fs.readFileSync
const rawReadFile = fs.readFile

async function atImport(options) {
let { css, load, resolve, from } = options
const fromParent = path.dirname(from)
const e1 = /\/\*[\s\S]*?\*\//g
// 匹配 // 单行注释,可能匹配到静态资源中的 http:// 的 //,不过影响不大, @import 不太可能出现在静态资源链接中
const e2 = /\/\/.*/g
// 使用正则匹配匹配出 多行注释和单行注释
const comments = []
let comment
while ((comment = e1.exec(css))) {
const [content] = comment
comments.push({
start: comment.index,
end: comment.index + content.length,
content: content
})
function rewriteFSForCss() {
proxyReadFileSync = function (path, options) {
return rawReadFileSync.call(fs, path, options)
}

while ((comment = e2.exec(css))) {
const [content] = comment
comments.push({
start: comment.index,
end: comment.index + content.length,
content: content
})
proxyReadFile = function (path, options, callback) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这两函数没意义

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

你下面写个const readfile = proxyReadfile||rawReadfile就行了

return rawReadFile.call(fs, path, options, callback)
}

// 排序方便二分
comments.sort((a, b) => (a.start > b.start ? 1 : -1))

function isInComments(index) {
let left = 0
let right = comments.length - 1

while (left <= right) {
const mid = Math.floor((left + right) / 2)
const comment = comments[mid]

if (index >= comment.start && index <= comment.end) {
return true
} else if (index < comment.start) {
right = mid - 1
} else {
left = mid + 1
}
}

return false
fs.readFileSync = function (path, options) {
return proxyReadFileSync(path, options)
}

// 使用正则表达式匹配出所有 @import 语法,语法包含 @import "path", @import 'path', @import url("path"), @import url('path')
// 注意清理分号,否则留个分号会报错
const importRegex = /@import\s+(url\(['"]([^'"]+)['"]\)|['"]([^'"]+)['"])(\s*;)?/g
let importList = []
let importMatch
while ((importMatch = importRegex.exec(css))) {
const fullMatch = importMatch[0]
const importSyntax = fullMatch.trim()
importSyntax.startsWith('@import')
const importValue = importSyntax.slice(7).trim()
// 匹配 @import 后字符串格式
const importUrlRegex = /url\s*\(['"]([^'"]+)['"]\)/g
const importStrRegexp = /^(['"])([^'"]+)\1/

let urlMatch = null
if (importValue.startsWith('url')) {
urlMatch = importUrlRegex.exec(importValue)?.[1]
} else {
urlMatch = importStrRegexp.exec(importValue)?.[2]
}
if (!urlMatch) {
continue
}

importList.push({
start: importMatch.index,
end: importMatch.index + fullMatch.length,
content: fullMatch,
url: urlMatch
})
}

// 过滤掉在注释中的 @import 语法
importList = importList.filter(imp => !isInComments(imp.start))

// 逆序替换 import,避免修改内容导致的索引偏移问题
importList.sort((a, b) => (a.start > b.start ? -1 : 1))

const result = await Promise.all(
importList.map(async imp => {
const importPath = imp.url
if (!importPath) return
// 非法路径直接报错
const resolvedUrl = await resolve(importPath, fromParent)
const content = (await load(resolvedUrl)) ?? ''
return {
content,
start: imp.start,
end: imp.end,
resolvedUrl
}
})
)

for (const res of result) {
if (!res) continue
css = css.slice(0, res.start) + '\n' + res.content + '\n' + css.slice(res.end)
}

return {
css,
imports: result.map(item => item.resolvedUrl)
fs.readFile = function (path, options, callback) {
return proxyReadFile(path, options, callback)
}
}
/**
* @param {StripByPostcssOption} options
*/
async function stripByPostcss(options) {
const defs = options.defs ?? {}

function stripContentCondition(content) {
content = stripCondition(content, defs)

if (options.lang === 'stylus') {
content = content.replace(/\t/g, ' ')
function startFSStripForCss(defs) {
function shouldStrip(path) {
return typeof path === 'string' && /\.(styl|scss|sass|less|css)$/.test(path)
}
proxyReadFileSync = function (path, options) {
const content = rawReadFileSync.call(fs, path, options)
if (shouldStrip(path)) {
try {
if (typeof content === 'string') {
return stripCondition(content, defs)
}
} catch (e) {
return content
}
}

return content
}

/**
* @type {string}
*/
const afterConditionStrip = stripContentCondition(options.css, defs)
const dependencies = []

const atImportOptions = {
async load(filename) {
let content = await fs.readFile(filename, 'utf-8')

content = stripContentCondition(content, defs)
proxyReadFile = function (path, options, callback) {
let cb = callback
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个你直接代理最后一个参数不就好了,不需要判断这些

if (typeof options === 'function') {
cb = options
options = null
}

const data = await atImport({
...atImportOptions,
from: filename,
css: content
})
dependencies.push(...data.imports)
return data.css
},
resolve: (id, base) => {
return new Promise((resolve, reject) => {
// 处理 ~ 开头的路径
options.resolve(base, id.startsWith('~') && !id.startsWith('~/') ? loaderUtils.urlToRequest(id) : id, (err, res) => {
if (err) return reject(err)
if (typeof res !== 'string') {
return reject(new Error(`[mpx-strip-conditional-loader]: Cannot resolve ${id} from ${base}`))
const wrappedCallback = (err, data) => {
if (err) return cb(err)
if (shouldStrip(path)) {
try {
if (typeof data === 'string') {
const result = stripCondition(data, defs)
return cb(null, result)
}
resolve(res)
})
})
} catch (e) {
return cb(null, data)
}
}
cb(null, data)
}
}

const result = await atImport({
...atImportOptions,
from: options.resourcePath,
css: afterConditionStrip
})

dependencies.push(...result.imports)

return {
css: result.css,
dependencies
if (options) {
return rawReadFile.call(fs, path, options, wrappedCallback)
}
return rawReadFile.call(fs, path, wrappedCallback)
}
}

const createResolver = (context, extensions) =>
context.getResolve({ mainFiles: ['index'], extensions: [...extensions, '.css'], preferRelative: true })
const resolver = {
stylus: context => createResolver(context, ['.styl']),
scss: context => createResolver(context, ['.scss']),
less: context => createResolver(context, ['.styl'])
}

/**
*
* @this {import('webpack').LoaderContext<any>}
Expand All @@ -360,22 +207,11 @@ module.exports = async function (css) {
const callback = this.async()

const mpx = this.getMpx()
const { resourcePath, queryObj } = parseRequest(this.resource)

const result = await stripByPostcss({
lang: queryObj.lang,
resourcePath,
css,
defs: mpx.defs,
resolve: resolver[queryObj.lang] ? resolver[queryObj.lang](this) : this.resolve.bind(this)
})

for (const dep of result.dependencies) {
this.addDependency(path.normalize(dep))
}
const result = stripCondition(css, mpx.defs)

callback(null, result.css, result.map)
callback(null, result)
}

module.exports.stripByPostcss = stripByPostcss
module.exports.stripCondition = stripCondition
module.exports.rewriteFSForCss = rewriteFSForCss
module.exports.startFSStripForCss = startFSStripForCss
Loading