UNPKG

@mpxjs/webpack-plugin

Version:

mpx compile core

374 lines (299 loc) 9.71 kB
const valueParser = require('postcss-value-parser') const { normalizeUrl, resolveRequests, isURLRequestable, requestify, WEBPACK_IGNORE_COMMENT_REGEXP } = require('../utils') const MPX_IMPORT_REGEXP = /^(@mpx-import\s+)/ // 例如匹配: ' @mpx-import "xxx"' function parseNode (atRule, key, options) { // Convert only top-level @import if (atRule.parent.type !== 'root') { return } if ( atRule.raws && atRule.raws.afterName && atRule.raws.afterName.trim().length > 0 ) { const lastCommentIndex = atRule.raws.afterName.lastIndexOf('/*') const matched = atRule.raws.afterName .slice(lastCommentIndex) .match(WEBPACK_IGNORE_COMMENT_REGEXP) if (matched && matched[2] === 'true') { return } } const prevNode = atRule.prev() if (prevNode && prevNode.type === 'comment') { const matched = prevNode.text.match(WEBPACK_IGNORE_COMMENT_REGEXP) if (matched && matched[2] === 'true') { return } } // Nodes do not exists - `@import url('http://') :root {}` if (atRule.nodes) { const error = new Error( 'It looks like you didn\'t end your @import statement correctly. Child nodes are attached to it.' ) error.node = atRule throw error } const rawParams = atRule.raws && atRule.raws[key] && typeof atRule.raws[key].raw !== 'undefined' ? atRule.raws[key].raw : atRule[key] const { nodes: paramsNodes } = valueParser(rawParams) // No nodes - `@import ;` // Invalid type - `@import foo-bar;` if ( paramsNodes.length === 0 || (paramsNodes[0].type !== 'string' && paramsNodes[0].type !== 'function') ) { const error = new Error(`Unable to find uri in "${atRule.toString()}"`) error.node = atRule throw error } let isStringValue let url if (paramsNodes[0].type === 'string') { isStringValue = true url = paramsNodes[0].value } else { // Invalid function - `@import nourl(test.css);` if (paramsNodes[0].value.toLowerCase() !== 'url') { const error = new Error(`Unable to find uri in "${atRule.toString()}"`) error.node = atRule throw error } isStringValue = paramsNodes[0].nodes.length !== 0 && paramsNodes[0].nodes[0].type === 'string' url = isStringValue ? paramsNodes[0].nodes[0].value : valueParser.stringify(paramsNodes[0].nodes) } url = normalizeUrl(url, isStringValue) const { requestable, needResolve } = isURLRequestable(url, options) let prefix if (requestable && needResolve) { const queryParts = url.split('!') if (queryParts.length > 1) { url = queryParts.pop() prefix = queryParts.join('!') } } // Empty url - `@import "";` or `@import url();` if (url.trim().length === 0) { const error = new Error(`Unable to find uri in "${atRule.toString()}"`) error.node = atRule throw error } const additionalNodes = paramsNodes.slice(1) let supports let layer let media if (additionalNodes.length > 0) { let nodes = [] for (const node of additionalNodes) { nodes.push(node) const isLayerFunction = node.type === 'function' && node.value.toLowerCase() === 'layer' const isLayerWord = node.type === 'word' && node.value.toLowerCase() === 'layer' if (isLayerFunction || isLayerWord) { if (isLayerFunction) { nodes.splice(nodes.length - 1, 1, ...node.nodes) } else { nodes.splice(nodes.length - 1, 1, { type: 'string', value: '', unclosed: false }) } layer = valueParser.stringify(nodes).trim().toLowerCase() nodes = [] } else if ( node.type === 'function' && node.value.toLowerCase() === 'supports' ) { nodes.splice(nodes.length - 1, 1, ...node.nodes) supports = valueParser.stringify(nodes).trim().toLowerCase() nodes = [] } } if (nodes.length > 0) { media = valueParser.stringify(nodes).trim().toLowerCase() } } // eslint-disable-next-line consistent-return return { atRule, prefix, url, layer, supports, media, requestable, needResolve } } const plugin = (options = {}) => { return { postcssPlugin: 'postcss-import-parser', prepare (result) { const parsedAtRules = [] return { Once (root, { AtRule }) { // Calls once per file, since every file has single Root // 遍历AST 找到注释节点(/* @mpx-import "xxx" */)进行@import 替换 root.walkComments((comment) => { if (MPX_IMPORT_REGEXP.test(comment.text)) { const importStatement = comment.text.replace(MPX_IMPORT_REGEXP, (matchStr, $1) => { return matchStr.replace($1, '') }) const matched = importStatement.match(/(["'].+["'])/) if (matched && matched[1]) { const url = matched[1] const importNode = new AtRule({ name: 'import', params: url, source: comment.source }) comment.before(importNode) comment.remove() } } }) }, AtRule: { import (atRule) { if (options.isCSSStyleSheet) { options.loaderContext.emitError( new Error( atRule.error( '\'@import\' rules are not allowed here and will not be processed' ).message ) ) return } const { isSupportDataURL, isSupportAbsoluteURL, externals, root } = options let parsedAtRule try { parsedAtRule = parseNode(atRule, 'params', { isSupportAbsoluteURL, isSupportDataURL, externals, root }) } catch (error) { result.warn(error.message, { node: error.node }) } if (!parsedAtRule) { return } parsedAtRules.push(parsedAtRule) } }, async OnceExit () { if (parsedAtRules.length === 0) { return } const { loaderContext } = options const resolver = loaderContext.getResolve({ dependencyType: 'css', conditionNames: ['style'], mainFields: ['css', 'style', 'main', '...'], mainFiles: ['index', '...'], extensions: ['.css', '...'], preferRelative: true }) const resolvedAtRules = await Promise.all( parsedAtRules.map(async (parsedAtRule) => { const { atRule, requestable, needResolve, prefix, url, layer, supports, media } = parsedAtRule if (options.filter) { const needKeep = await options.filter( url, media, loaderContext.resourcePath, supports, layer ) if (!needKeep) { return } } if (needResolve) { const request = requestify(url, loaderContext.rootContext) const resolvedUrl = await resolveRequests( resolver, loaderContext.context, [...new Set([request, url])] ) if (!resolvedUrl) { return } if (resolvedUrl === loaderContext.resourcePath) { atRule.remove() return } atRule.remove() // eslint-disable-next-line consistent-return return { url: resolvedUrl, layer, supports, media, prefix, requestable } } atRule.remove() // eslint-disable-next-line consistent-return return { url, layer, supports, media, prefix, requestable } }) ) const urlToNameMap = new Map() for (let index = 0; index <= resolvedAtRules.length - 1; index++) { const resolvedAtRule = resolvedAtRules[index] if (!resolvedAtRule) { // eslint-disable-next-line no-continue continue } const { url, requestable, layer, supports, media } = resolvedAtRule if (!requestable) { options.api.push({ url, layer, supports, media, index }) // eslint-disable-next-line no-continue continue } const { prefix } = resolvedAtRule const newUrl = prefix ? `${prefix}!${url}` : url let importName = urlToNameMap.get(newUrl) if (!importName) { importName = `___CSS_LOADER_AT_RULE_IMPORT_${urlToNameMap.size}___` urlToNameMap.set(newUrl, importName) options.imports.push({ type: 'rule_import', importName, url: options.urlHandler(newUrl), index }) options.api.push({ importName, layer, supports, media, index }) } } } } } } } plugin.postcss = true module.exports = plugin