-
Notifications
You must be signed in to change notification settings - Fork 393
feat: rewrite fs for css #2369
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat: rewrite fs for css #2369
Changes from 3 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
e20aa5a
feat: rewrite fs for css
Blackgan3 c1e6ecc
fix: lint error
Blackgan3 fa451cf
feat: 修改rewriteFs逻辑
Blackgan3 f3183db
feat: 支持多种格式注释&支持Mpx文件过滤
Blackgan3 29e0a08
feat: 调整添加init-mpx文件进行初始化工作
Blackgan3 372cfc4
fix: path error
Blackgan3 e840b6b
fix: unit test error
Blackgan3 d180832
feat: change init-mpx to init
Blackgan3 4ad6b44
Merge branch 'master' into feat-css-if-strip-override-fs
hiyuki File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) { | ||
|
|
@@ -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 | ||
| } | ||
|
|
@@ -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 | ||
|
|
@@ -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') { | ||
|
|
@@ -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 | ||
|
|
@@ -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 || '' | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -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) { | ||
| 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 | ||
|
||
| 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>} | ||
|
|
@@ -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 | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这两函数没意义
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
你下面写个const readfile = proxyReadfile||rawReadfile就行了