postcss-url-parser.js 6.53 KB
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;

var _util = require("util");

var _postcss = _interopRequireDefault(require("postcss"));

var _postcssValueParser = _interopRequireDefault(require("postcss-value-parser"));

var _utils = require("../utils");

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

const pluginName = 'postcss-url-parser';
const isUrlFunc = /url/i;
const isImageSetFunc = /^(?:-webkit-)?image-set$/i;
const needParseDecl = /(?:url|(?:-webkit-)?image-set)\(/i;

function getNodeFromUrlFunc(node) {
  return node.nodes && node.nodes[0];
}

function shouldHandleRule(rule, decl, result) {
  // https://www.w3.org/TR/css-syntax-3/#typedef-url-token
  if (rule.url.replace(/^[\s]+|[\s]+$/g, '').length === 0) {
    result.warn(`Unable to find uri in '${decl.toString()}'`, {
      node: decl
    });
    return false;
  }

  if (!(0, _utils.isUrlRequestable)(rule.url)) {
    return false;
  }

  return true;
}

function walkCss(css, result, options, callback) {
  const accumulator = [];
  css.walkDecls(decl => {
    if (!needParseDecl.test(decl.value)) {
      return;
    }

    const parsed = (0, _postcssValueParser.default)(decl.value);
    parsed.walk(node => {
      if (node.type !== 'function') {
        return;
      }

      if (isUrlFunc.test(node.value)) {
        const {
          nodes
        } = node;
        const isStringValue = nodes.length !== 0 && nodes[0].type === 'string';
        const url = isStringValue ? nodes[0].value : _postcssValueParser.default.stringify(nodes);
        const rule = {
          node: getNodeFromUrlFunc(node),
          url,
          needQuotes: false,
          isStringValue
        };

        if (shouldHandleRule(rule, decl, result)) {
          accumulator.push({
            decl,
            rule,
            parsed
          });
        } // Do not traverse inside `url`
        // eslint-disable-next-line consistent-return


        return false;
      } else if (isImageSetFunc.test(node.value)) {
        for (const nNode of node.nodes) {
          const {
            type,
            value
          } = nNode;

          if (type === 'function' && isUrlFunc.test(value)) {
            const {
              nodes
            } = nNode;
            const isStringValue = nodes.length !== 0 && nodes[0].type === 'string';
            const url = isStringValue ? nodes[0].value : _postcssValueParser.default.stringify(nodes);
            const rule = {
              node: getNodeFromUrlFunc(nNode),
              url,
              needQuotes: false,
              isStringValue
            };

            if (shouldHandleRule(rule, decl, result)) {
              accumulator.push({
                decl,
                rule,
                parsed
              });
            }
          } else if (type === 'string') {
            const rule = {
              node: nNode,
              url: value,
              needQuotes: true,
              isStringValue: true
            };

            if (shouldHandleRule(rule, decl, result)) {
              accumulator.push({
                decl,
                rule,
                parsed
              });
            }
          }
        } // Do not traverse inside `image-set`
        // eslint-disable-next-line consistent-return


        return false;
      }
    });
  });
  callback(null, accumulator);
}

const asyncWalkCss = (0, _util.promisify)(walkCss);

var _default = _postcss.default.plugin(pluginName, options => async (css, result) => {
  const parsedResults = await asyncWalkCss(css, result, options);

  if (parsedResults.length === 0) {
    return Promise.resolve();
  }

  const tasks = [];
  const imports = new Map();
  const replacements = new Map();
  let hasUrlImportHelper = false;

  for (const parsedResult of parsedResults) {
    const {
      url,
      isStringValue
    } = parsedResult.rule;
    let normalizedUrl = url;
    let prefix = '';
    const queryParts = normalizedUrl.split('!');

    if (queryParts.length > 1) {
      normalizedUrl = queryParts.pop();
      prefix = queryParts.join('!');
    }

    normalizedUrl = (0, _utils.normalizeUrl)(normalizedUrl, isStringValue);

    if (!options.filter(normalizedUrl)) {
      // eslint-disable-next-line no-continue
      continue;
    }

    if (!hasUrlImportHelper) {
      options.imports.push({
        importName: '___CSS_LOADER_GET_URL_IMPORT___',
        url: options.urlHandler(require.resolve('../runtime/getUrl.js')),
        index: -1
      });
      hasUrlImportHelper = true;
    }

    const splittedUrl = normalizedUrl.split(/(\?)?#/);
    const [pathname, query, hashOrQuery] = splittedUrl;
    let hash = query ? '?' : '';
    hash += hashOrQuery ? `#${hashOrQuery}` : '';
    const request = (0, _utils.requestify)(pathname, options.rootContext);
    tasks.push((async () => {
      const {
        resolver,
        context
      } = options;
      const resolvedUrl = await (0, _utils.resolveRequests)(resolver, context, [...new Set([request, normalizedUrl])]);
      return {
        url: resolvedUrl,
        prefix,
        hash,
        parsedResult
      };
    })());
  }

  const results = await Promise.all(tasks);

  for (let index = 0; index <= results.length - 1; index++) {
    const {
      url,
      prefix,
      hash,
      parsedResult: {
        decl,
        rule,
        parsed
      }
    } = results[index];
    const newUrl = prefix ? `${prefix}!${url}` : url;
    const importKey = newUrl;
    let importName = imports.get(importKey);

    if (!importName) {
      importName = `___CSS_LOADER_URL_IMPORT_${imports.size}___`;
      imports.set(importKey, importName);
      options.imports.push({
        importName,
        url: options.urlHandler(newUrl),
        index
      });
    }

    const {
      needQuotes
    } = rule;
    const replacementKey = JSON.stringify({
      newUrl,
      hash,
      needQuotes
    });
    let replacementName = replacements.get(replacementKey);

    if (!replacementName) {
      replacementName = `___CSS_LOADER_URL_REPLACEMENT_${replacements.size}___`;
      replacements.set(replacementKey, replacementName);
      options.replacements.push({
        replacementName,
        importName,
        hash,
        needQuotes
      });
    } // eslint-disable-next-line no-param-reassign


    rule.node.type = 'word'; // eslint-disable-next-line no-param-reassign

    rule.node.value = replacementName; // eslint-disable-next-line no-param-reassign

    decl.value = parsed.toString();
  }

  return Promise.resolve();
});

exports.default = _default;