From: Jakub Pawlowicz Date: Sun, 11 Dec 2016 10:50:11 +0000 (+0100) Subject: Restores source map support. X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=92bab846edc9657dc5ef1130f6c4350fab00f015;p=clean-css.git Restores source map support. Re-built from ground up on top of the new tokenizing process it features simpler rebasing while still supporting all features of the previous solution. --- diff --git a/lib/clean.js b/lib/clean.js index de987a11..091fa0d0 100644 --- a/lib/clean.js +++ b/lib/clean.js @@ -13,6 +13,7 @@ var Validator = require('./properties/validator'); var override = require('./utils/override'); var DEFAULT_TIMEOUT = 5000; +var inputSourceMapTracker = require('./utils/input-source-map-tracker'); var readSources = require('./utils/read-sources'); var basicOptimize = require('./optimizer/basic'); var advancedOptimize = require('./optimizer/advanced'); @@ -74,12 +75,19 @@ CleanCSS.prototype.minify = function (input, callback) { timeSpent: 0 }, errors: [], - warnings: [], - options: this.options, + inputSourceMapTracker: inputSourceMapTracker(), localOnly: !callback, - validator: new Validator(this.options.compatibility) + options: this.options, + source: null, + sourcesContent: {}, + validator: new Validator(this.options.compatibility), + warnings: [] }; + if (typeof this.options.sourceMap == 'string') { + context.inputSourceMapTracker.track(undefined, this.options.sourceMap); + } + return runner(context.localOnly)(function () { return readSources(input, context, function (tokens) { var stringify = context.options.sourceMap ? diff --git a/lib/properties/shorthand-compactor.js b/lib/properties/shorthand-compactor.js index 5ee882a1..5c2b2cff 100644 --- a/lib/properties/shorthand-compactor.js +++ b/lib/properties/shorthand-compactor.js @@ -20,23 +20,28 @@ function mixedImportance(components) { return false; } -function componentSourceMaps(components) { - var sourceMapping = []; - - for (var name in components) { - var component = components[name]; - var originalValue = component.all[component.position]; - var mapping = originalValue[0][originalValue[0].length - 1]; - - if (Array.isArray(mapping)) - Array.prototype.push.apply(sourceMapping, mapping); +function joinMetadata(components, at) { + var metadata = []; + var component; + var originalValue; + var componentMetadata; + var name; + + for (name in components) { + component = components[name]; + originalValue = component.all[component.position]; + componentMetadata = originalValue[at][originalValue[at].length - 1]; + + Array.prototype.push.apply(metadata, componentMetadata); } - return sourceMapping; + return metadata; } function replaceWithShorthand(properties, candidateComponents, name, validator) { var descriptor = compactable[name]; + var nameMetadata; + var valueMetadata; var newValuePlaceholder = [ Token.PROPERTY, [Token.PROPERTY_NAME, name], @@ -70,10 +75,11 @@ function replaceWithShorthand(properties, candidateComponents, name, validator) candidateComponents[componentName].unused = true; } - // var sourceMapping = componentSourceMaps(candidateComponents); - // if (sourceMapping.length > 0) - // newValuePlaceholder[0].push(sourceMapping); - // } + nameMetadata = joinMetadata(candidateComponents, 1); + newValuePlaceholder[1].push(nameMetadata); + + valueMetadata = joinMetadata(candidateComponents, 2); + newValuePlaceholder[2].push(valueMetadata); newProperty.position = all.length; newProperty.all = all; diff --git a/lib/source-maps/track.js b/lib/source-maps/track.js deleted file mode 100644 index 735a58dc..00000000 --- a/lib/source-maps/track.js +++ /dev/null @@ -1,119 +0,0 @@ -var escapePrefix = '__ESCAPED_'; - -function trackPrefix(value, context, interestingContent) { - if (!interestingContent && value.indexOf('\n') == -1) { - if (value.indexOf(escapePrefix) === 0) { - return value; - } else { - context.column += value.length; - return; - } - } - - var withoutContent = 0; - var split = value.split('\n'); - var total = split.length; - var shift = 0; - - while (true) { - if (withoutContent == total - 1) - break; - - var part = split[withoutContent]; - if (/\S/.test(part)) - break; - - shift += part.length + 1; - withoutContent++; - } - - context.line += withoutContent; - context.column = withoutContent > 0 ? 0 : context.column; - context.column += /^(\s)*/.exec(split[withoutContent])[0].length; - - return value.substring(shift).trimLeft(); -} - -function sourceFor(originalMetadata, contextMetadata, context) { - var source = originalMetadata.source || contextMetadata.source; - - if (source && context.resolvePath) - return context.resolvePath(contextMetadata.source, source); - - return source; -} - -function snapshot(data, context, fallbacks) { - var metadata = { - line: context.line, - column: context.column, - source: context.source - }; - var sourceContent = null; - var sourceMetadata = context.sourceMapTracker.isTracking(metadata.source) ? - context.sourceMapTracker.originalPositionFor(metadata, data, fallbacks || 0) : - {}; - - metadata.line = sourceMetadata.line || metadata.line; - metadata.column = sourceMetadata.column || metadata.column; - metadata.source = sourceMetadata.sourceResolved ? - sourceMetadata.source : - sourceFor(sourceMetadata, metadata, context); - - if (context.sourceMapInlineSources) { - var sourceMapSourcesContent = context.sourceMapTracker.sourcesContentFor(context.source); - sourceContent = sourceMapSourcesContent && sourceMapSourcesContent[metadata.source] ? - sourceMapSourcesContent : - context.sourceReader.sourceAt(context.source); - } - - return sourceContent ? - [metadata.line, metadata.column, metadata.source, sourceContent] : - [metadata.line, metadata.column, metadata.source]; -} - -function trackSuffix(data, context) { - var parts = data.split('\n'); - - for (var i = 0, l = parts.length; i < l; i++) { - var part = parts[i]; - var cursor = 0; - - if (i > 0) { - context.line++; - context.column = 0; - } - - while (true) { - var next = part.indexOf(escapePrefix, cursor); - - if (next == -1) { - context.column += part.substring(cursor).length; - break; - } - - context.column += next - cursor; - cursor += next - cursor; - - var escaped = part.substring(next, part.indexOf('__', next + 1) + 2); - var encodedValues = escaped.substring(escaped.indexOf('(') + 1, escaped.indexOf(')')).split(','); - context.line += ~~encodedValues[0]; - context.column = (~~encodedValues[0] === 0 ? context.column : 0) + ~~encodedValues[1]; - cursor += escaped.length; - } - } -} - -function track(data, context, snapshotMetadata, fallbacks) { - var untracked = trackPrefix(data, context, snapshotMetadata); - var metadata = snapshotMetadata ? - snapshot(untracked, context, fallbacks) : - []; - - if (untracked) - trackSuffix(untracked, context); - - return metadata; -} - -module.exports = track; diff --git a/lib/stringifier/helpers.js b/lib/stringifier/helpers.js index 0cc788d8..a020cbea 100644 --- a/lib/stringifier/helpers.js +++ b/lib/stringifier/helpers.js @@ -127,7 +127,6 @@ function all(tokens, context) { break; case Token.AT_RULE_BLOCK: rules(token[1], context); - store(joinCharacter, context); store(Marker.OPEN_BRACE, context); body(token[2], context); store(Marker.CLOSE_BRACE, context); @@ -136,6 +135,7 @@ function all(tokens, context) { rules(token[1], context); store(Marker.OPEN_BRACE, context); all(token[2], context); + store(joinCharacter, context); store(Marker.CLOSE_BRACE, context); break; case Token.COMMENT: diff --git a/lib/stringifier/source-maps.js b/lib/stringifier/source-maps.js index 4b62a5be..14de1804 100644 --- a/lib/stringifier/source-maps.js +++ b/lib/stringifier/source-maps.js @@ -1,85 +1,74 @@ var SourceMapGenerator = require('source-map').SourceMapGenerator; var all = require('./helpers').all; +var isRemoteResource = require('../utils/is-remote-resource'); + var isWindows = process.platform == 'win32'; -var unknownSource = '$stdin'; +var UNKNOWN_SOURCE = '$stdin'; -function store(element, context) { +function store(element, stringifyContext) { var fromString = typeof element == 'string'; var value = fromString ? element : element[1]; + var mappings = fromString ? null : element[2]; - if (value.indexOf('_') > -1) - value = context.restore(value, prefixContentFrom(context.output)); - - track(value, fromString ? null : element, context); - context.output.push(value); + track(value, mappings, stringifyContext); + stringifyContext.output.push(value); } -function prefixContentFrom(values) { - var content = []; - - for (var i = values.length - 1; i >= 0; i--) { - var value = values[i]; - content.unshift(value); +function track(value, mappings, stringifyContext) { + var parts = value.split('\n'); - if (value == '{' || value == ';') - break; + if (mappings) { + trackAllMappings(mappings, stringifyContext); } - return content.join(''); -} - -function track(value, element, context) { - if (element) - trackAllMappings(element, context); - - var parts = value.split('\n'); - context.line += parts.length - 1; - context.column = parts.length > 1 ? 0 : (context.column + parts.pop().length); + stringifyContext.line += parts.length - 1; + stringifyContext.column = parts.length > 1 ? 0 : (stringifyContext.column + parts.pop().length); } -function trackAllMappings(element, context) { - var mapping = element[element.length - 1]; - - if (!Array.isArray(mapping)) - return; - - for (var i = 0, l = mapping.length; i < l; i++) { - trackMapping(mapping[i], context); +function trackAllMappings(mappings, stringifyContext) { + for (var i = 0, l = mappings.length; i < l; i++) { + trackMapping(mappings[i], stringifyContext); } } -function trackMapping(mapping, context) { - var source = mapping[2] || unknownSource; +function trackMapping(mapping, stringifyContext) { + var line = mapping[0]; + var column = mapping[1]; + var originalSource = mapping[2]; + var source = originalSource; + var storedSource = source || UNKNOWN_SOURCE; - if (isWindows) + if (isWindows && source && !isRemoteResource(source)) { source = source.replace(/\\/g, '/'); + } - context.outputMap.addMapping({ + stringifyContext.outputMap.addMapping({ generated: { - line: context.line, - column: context.column + line: stringifyContext.line, + column: stringifyContext.column }, - source: source, + source: storedSource, original: { - line: mapping[0], - column: mapping[1] + line: line, + column: column } }); - if (mapping[3]) - context.outputMap.setSourceContent(source, mapping[3][mapping[2]]); + if (stringifyContext.inlineSources && (originalSource in stringifyContext.sourcesContent)) { + stringifyContext.outputMap.setSourceContent(storedSource, stringifyContext.sourcesContent[originalSource]); + } } function stringify(tokens, context) { var stringifyContext = { column: 0, - inputMapTracker: context.inputMapTracker, keepBreaks: context.options.keepBreaks, + inlineSources: context.options.sourceMapInlineSources, line: 1, output: [], outputMap: new SourceMapGenerator(), - sourceMapInlineSources: context.options.sourceMapInlineSources, + sourcesContent: context.sourcesContent, spaceAfterClosingBrace: context.options.compatibility.properties.spaceAfterClosingBrace, store: store }; diff --git a/lib/tokenizer/tokenize.js b/lib/tokenizer/tokenize.js index ac8a4cb4..9b4cf117 100644 --- a/lib/tokenizer/tokenize.js +++ b/lib/tokenizer/tokenize.js @@ -55,6 +55,7 @@ function intoTokens(source, externalContext, internalContext, isNested) { var levels = []; var buffer = []; var buffers = []; + var serializedBuffer; var roundBracketLevel = 0; var isQuoted; var isSpace; @@ -79,7 +80,7 @@ function intoTokens(source, externalContext, internalContext, isNested) { isCommentEnd = !wasCommentStart && level == Level.COMMENT && character == Marker.FORWARD_SLASH && source[position.index - 1] == Marker.STAR; metadata = buffer.length === 0 ? - metadataFrom(position, 0, externalContext) : + [position.line, position.column, position.source] : metadata; if (isEscaped) { @@ -94,7 +95,7 @@ function intoTokens(source, externalContext, internalContext, isNested) { buffers.push(buffer.slice(0, buffer.length - 2)); buffer = buffer.slice(buffer.length - 2); - metadata = metadataFrom(position, 1, externalContext); + metadata = [position.line, position.column - 1, position.source]; levels.push(level); level = Level.COMMENT; @@ -105,7 +106,8 @@ function intoTokens(source, externalContext, internalContext, isNested) { buffer.push(character); } else if (isCommentEnd) { // comment end, e.g. /* comment */<-- - lastToken = [Token.COMMENT, buffer.join('').trim() + character, [metadata]]; + serializedBuffer = buffer.join('').trim() + character; + lastToken = [Token.COMMENT, serializedBuffer, [originalMetadata(metadata, serializedBuffer, externalContext)]]; newTokens.push(lastToken); level = levels.pop(); @@ -144,12 +146,14 @@ function intoTokens(source, externalContext, internalContext, isNested) { roundBracketLevel--; } else if (character == Marker.SEMICOLON && level == Level.BLOCK) { // semicolon ending rule at block level, e.g. @import '...';<-- - allTokens.push([Token.AT_RULE, buffer.join('').trim(), [metadata]]); + serializedBuffer = buffer.join('').trim(); + allTokens.push([Token.AT_RULE, serializedBuffer, [originalMetadata(metadata, serializedBuffer, externalContext)]]); buffer = []; } else if (character == Marker.COMMA && level == Level.BLOCK && ruleToken) { // comma separator at block level, e.g. a,div,<-- - ruleToken[1].push([tokenScopeFrom(ruleToken[0]), buffer.join('').trim(), [metadata]]); + serializedBuffer = buffer.join('').trim(); + ruleToken[1].push([tokenScopeFrom(ruleToken[0]), serializedBuffer, [originalMetadata(metadata, serializedBuffer, externalContext, ruleToken[1].length)]]); buffer = []; } else if (character == Marker.COMMA && level == Level.BLOCK && tokenTypeFrom(buffer) == Token.AT_RULE) { @@ -159,12 +163,14 @@ function intoTokens(source, externalContext, internalContext, isNested) { } else if (character == Marker.COMMA && level == Level.BLOCK) { // comma separator at block level, e.g. a,<-- ruleToken = [tokenTypeFrom(buffer), [], []]; - ruleToken[1].push([tokenScopeFrom(ruleToken[0]), buffer.join('').trim(), [metadata]]); + serializedBuffer = buffer.join('').trim(); + ruleToken[1].push([tokenScopeFrom(ruleToken[0]), serializedBuffer, [originalMetadata(metadata, serializedBuffer, externalContext, 0)]]); buffer = []; } else if (character == Marker.OPEN_BRACE && level == Level.BLOCK && ruleToken && ruleToken[0] == Token.BLOCK) { // open brace opening at-rule at block level, e.g. @media{<-- - ruleToken[1].push([Token.BLOCK_SCOPE, buffer.join('').trim(), [metadata]]); + serializedBuffer = buffer.join('').trim(); + ruleToken[1].push([Token.BLOCK_SCOPE, serializedBuffer, [originalMetadata(metadata, serializedBuffer, externalContext)]]); allTokens.push(ruleToken); levels.push(level); @@ -176,8 +182,9 @@ function intoTokens(source, externalContext, internalContext, isNested) { ruleToken = null; } else if (character == Marker.OPEN_BRACE && level == Level.BLOCK && tokenTypeFrom(buffer) == Token.BLOCK) { // open brace opening at-rule at block level, e.g. @media{<-- + serializedBuffer = buffer.join('').trim(); ruleToken = ruleToken || [Token.BLOCK, [], []]; - ruleToken[1].push([Token.BLOCK_SCOPE, buffer.join('').trim(), [metadata]]); + ruleToken[1].push([Token.BLOCK_SCOPE, serializedBuffer, [originalMetadata(metadata, serializedBuffer, externalContext)]]); allTokens.push(ruleToken); levels.push(level); @@ -189,8 +196,9 @@ function intoTokens(source, externalContext, internalContext, isNested) { ruleToken = null; } else if (character == Marker.OPEN_BRACE && level == Level.BLOCK) { // open brace opening rule at block level, e.g. div{<-- + serializedBuffer = buffer.join('').trim(); ruleToken = ruleToken || [tokenTypeFrom(buffer), [], []]; - ruleToken[1].push([tokenScopeFrom(ruleToken[0]), buffer.join('').trim(), [metadata]]); + ruleToken[1].push([tokenScopeFrom(ruleToken[0]), serializedBuffer, [originalMetadata(metadata, serializedBuffer, externalContext, ruleToken[1].length)]]); newTokens = ruleToken[2]; allTokens.push(ruleToken); @@ -209,19 +217,22 @@ function intoTokens(source, externalContext, internalContext, isNested) { seekingValue = false; } else if (character == Marker.COLON && level == Level.RULE && !seekingValue) { // colon at rule level, e.g. a{color:<-- - propertyToken = [Token.PROPERTY, [Token.PROPERTY_NAME, buffer.join('').trim(), [metadata]]]; + serializedBuffer = buffer.join('').trim(); + propertyToken = [Token.PROPERTY, [Token.PROPERTY_NAME, serializedBuffer, [originalMetadata(metadata, serializedBuffer, externalContext)]]]; newTokens.push(propertyToken); seekingValue = true; buffer = []; } else if (character == Marker.SEMICOLON && level == Level.RULE && propertyToken && ruleTokens.length > 0 && buffer.length > 0 && buffer[0] == Marker.AT) { // semicolon at rule level for at-rule, e.g. a{--color:{@apply(--other-color);<-- - ruleToken[1].push([Token.AT_RULE, buffer.join('').trim(), [metadata]]); + serializedBuffer = buffer.join('').trim(); + ruleToken[1].push([Token.AT_RULE, serializedBuffer, [originalMetadata(metadata, serializedBuffer, externalContext)]]); buffer = []; } else if (character == Marker.SEMICOLON && level == Level.RULE && propertyToken && buffer.length > 0) { // semicolon at rule level, e.g. a{color:red;<-- - propertyToken.push([Token.PROPERTY_VALUE, buffer.join('').trim(), [metadata]]); + serializedBuffer = buffer.join('').trim(); + propertyToken.push([Token.PROPERTY_VALUE, serializedBuffer, [originalMetadata(metadata, serializedBuffer, externalContext)]]); seekingValue = false; buffer = []; @@ -230,7 +241,8 @@ function intoTokens(source, externalContext, internalContext, isNested) { seekingValue = false; } else if (character == Marker.SEMICOLON && level == Level.RULE && buffer.length > 0 && buffer[0] == Marker.AT) { // semicolon for at-rule at rule level, e.g. a{@apply(--variable);<-- - newTokens.push([Token.AT_RULE, buffer.join('').trim(), [metadata]]); + serializedBuffer = buffer.join(''); + newTokens.push([Token.AT_RULE, serializedBuffer, [originalMetadata(metadata, serializedBuffer, externalContext)]]); seekingValue = false; buffer = []; @@ -239,7 +251,8 @@ function intoTokens(source, externalContext, internalContext, isNested) { // noop } else if (character == Marker.CLOSE_BRACE && level == Level.RULE && propertyToken && seekingValue && buffer.length > 0 && ruleTokens.length > 0) { // close brace at rule level, e.g. a{--color:{color:red}<-- - propertyToken.push([Token.PROPERTY_VALUE, buffer.join(''), [metadata]]); + serializedBuffer = buffer.join(''); + propertyToken.push([Token.PROPERTY_VALUE, serializedBuffer, [originalMetadata(metadata, serializedBuffer, externalContext)]]); propertyToken = null; ruleToken = ruleTokens.pop(); newTokens = ruleToken[2]; @@ -249,7 +262,8 @@ function intoTokens(source, externalContext, internalContext, isNested) { buffer = []; } else if (character == Marker.CLOSE_BRACE && level == Level.RULE && propertyToken && buffer.length > 0 && buffer[0] == Marker.AT && ruleTokens.length > 0) { // close brace at rule level for at-rule, e.g. a{--color:{@apply(--other-color)}<-- - ruleToken[1].push([Token.AT_RULE, buffer.join(''), [metadata]]); + serializedBuffer = buffer.join(''); + ruleToken[1].push([Token.AT_RULE, serializedBuffer, [originalMetadata(metadata, serializedBuffer, externalContext)]]); propertyToken = null; ruleToken = ruleTokens.pop(); newTokens = ruleToken[2]; @@ -267,7 +281,8 @@ function intoTokens(source, externalContext, internalContext, isNested) { seekingValue = false; } else if (character == Marker.CLOSE_BRACE && level == Level.RULE && propertyToken && buffer.length > 0) { // close brace at rule level, e.g. a{color:red}<-- - propertyToken.push([Token.PROPERTY_VALUE, buffer.join(''), [metadata]]); + serializedBuffer = buffer.join(''); + propertyToken.push([Token.PROPERTY_VALUE, serializedBuffer, [originalMetadata(metadata, serializedBuffer, externalContext)]]); propertyToken = null; ruleToken = ruleTokens.pop(); newTokens = allTokens; @@ -278,7 +293,8 @@ function intoTokens(source, externalContext, internalContext, isNested) { } else if (character == Marker.CLOSE_BRACE && level == Level.RULE && buffer.length > 0 && buffer[0] == Marker.AT) { // close brace after at-rule at rule level, e.g. a{@apply(--variable)}<-- ruleToken = null; - newTokens.push([Token.AT_RULE, buffer.join('').trim(), [metadata]]); + serializedBuffer = buffer.join('').trim(); + newTokens.push([Token.AT_RULE, serializedBuffer, [originalMetadata(metadata, serializedBuffer, externalContext)]]); newTokens = allTokens; level = levels.pop(); @@ -305,7 +321,8 @@ function intoTokens(source, externalContext, internalContext, isNested) { } else if (character == Marker.CLOSE_ROUND_BRACKET && level == Level.RULE && seekingValue && roundBracketLevel == 1) { // round close bracket, e.g. a{color:hsla(0,0%,0%)<-- buffer.push(character); - propertyToken.push([Token.PROPERTY_VALUE, buffer.join('').trim(), [metadata]]); + serializedBuffer = buffer.join('').trim(); + propertyToken.push([Token.PROPERTY_VALUE, serializedBuffer, [originalMetadata(metadata, serializedBuffer, externalContext)]]); roundBracketLevel--; buffer = []; @@ -315,34 +332,38 @@ function intoTokens(source, externalContext, internalContext, isNested) { roundBracketLevel--; } else if (character == Marker.FORWARD_SLASH && source[position.index + 1] != Marker.STAR && level == Level.RULE && seekingValue && buffer.length > 0) { // forward slash within a property, e.g. a{background:url(image.png) 0 0/<-- - propertyToken.push([Token.PROPERTY_VALUE, buffer.join('').trim(), [metadata]]); - propertyToken.push([Token.PROPERTY_VALUE, character, [metadataFrom(position, 0, externalContext)]]); + serializedBuffer = buffer.join('').trim(); + propertyToken.push([Token.PROPERTY_VALUE, serializedBuffer, [originalMetadata(metadata, serializedBuffer, externalContext)]]); + propertyToken.push([Token.PROPERTY_VALUE, character, [[position.line, position.column, position.source]]]); buffer = []; } else if (character == Marker.FORWARD_SLASH && source[position.index + 1] != Marker.STAR && level == Level.RULE && seekingValue) { // forward slash within a property after space, e.g. a{background:url(image.png) 0 0 /<-- - propertyToken.push([Token.PROPERTY_VALUE, character, [metadataFrom(position, 0, externalContext)]]); + propertyToken.push([Token.PROPERTY_VALUE, character, [[position.line, position.column, position.source]]]); buffer = []; } else if (character == Marker.COMMA && level == Level.RULE && seekingValue && buffer.length > 0) { // comma within a property, e.g. a{background:url(image.png),<-- - propertyToken.push([Token.PROPERTY_VALUE, buffer.join('').trim(), [metadata]]); - propertyToken.push([Token.PROPERTY_VALUE, character, [metadataFrom(position, 0, externalContext)]]); + serializedBuffer = buffer.join('').trim(); + propertyToken.push([Token.PROPERTY_VALUE, serializedBuffer, [originalMetadata(metadata, serializedBuffer, externalContext)]]); + propertyToken.push([Token.PROPERTY_VALUE, character, [[position.line, position.column, position.source]]]); buffer = []; } else if (character == Marker.COMMA && level == Level.RULE && seekingValue) { // comma within a property after space, e.g. a{background:url(image.png) ,<-- - propertyToken.push([Token.PROPERTY_VALUE, character, [metadataFrom(position, 0, externalContext)]]); + propertyToken.push([Token.PROPERTY_VALUE, character, [[position.line, position.column, position.source]]]); buffer = []; } else if ((isSpace || (isNewLineNix && !isNewLineWin)) && level == Level.RULE && seekingValue && propertyToken && buffer.length > 0) { // space or *nix newline within property, e.g. a{margin:0 <-- - propertyToken.push([Token.PROPERTY_VALUE, buffer.join('').trim(), [metadata]]); + serializedBuffer = buffer.join('').trim(); + propertyToken.push([Token.PROPERTY_VALUE, serializedBuffer, [originalMetadata(metadata, serializedBuffer, externalContext)]]); buffer = []; } else if (isNewLineWin && level == Level.RULE && seekingValue && propertyToken && buffer.length > 1) { // win newline within property, e.g. a{margin:0\r\n<-- - propertyToken.push([Token.PROPERTY_VALUE, buffer.join('').trim(), [metadata]]); + serializedBuffer = buffer.join('').trim(); + propertyToken.push([Token.PROPERTY_VALUE, serializedBuffer, [originalMetadata(metadata, serializedBuffer, externalContext)]]); buffer = []; } else if (isNewLineWin && level == Level.RULE && seekingValue) { @@ -369,19 +390,19 @@ function intoTokens(source, externalContext, internalContext, isNested) { } if (seekingValue && buffer.length > 0) { - propertyToken.push([Token.PROPERTY_VALUE, buffer.join('').replace(TAIL_BROKEN_VALUE_PATTERN, ''), [metadata]]); + serializedBuffer = buffer.join('').replace(TAIL_BROKEN_VALUE_PATTERN, ''); + propertyToken.push([Token.PROPERTY_VALUE, serializedBuffer, [originalMetadata(metadata, serializedBuffer, externalContext)]]); } return allTokens; } -function metadataFrom(position, columnDelta, externalContext) { - var metadata = [position.line, position.column - columnDelta, position.source]; - metadata = externalContext.inputSourceMap ? - externalContext.inputSourceMapTracker.originalPositionFor(metadata) : - metadata; +function originalMetadata(metadata, value, externalContext, selectorFallbacks) { + var source = metadata[2]; - return metadata; + return externalContext.inputSourceMapTracker.isTracking(source) ? + externalContext.inputSourceMapTracker.originalPositionFor(metadata, value.length, selectorFallbacks) : + metadata; } function tokenTypeFrom(buffer) { diff --git a/lib/urls/rewrite.js b/lib/urls/rewrite.js index daced3d5..4fefbafb 100644 --- a/lib/urls/rewrite.js +++ b/lib/urls/rewrite.js @@ -95,7 +95,7 @@ function hasRoundBrackets(url) { return ROUND_BRACKETS_PATTERN.test(url); } -function rewriteUrl(originalUrl, rebaseConfig) { +function rewriteUrl(originalUrl, rebaseConfig, pathOnly) { var strippedUrl = originalUrl .replace(URL_PREFIX_PATTERN, '') .replace(URL_SUFFIX_PATTERN, '') @@ -109,7 +109,9 @@ function rewriteUrl(originalUrl, rebaseConfig) { strippedUrl[0] : quoteFor(unquotedUrl); - return URL_PREFIX + quote + rebase(unquotedUrl, rebaseConfig) + quote + URL_SUFFIX; + return pathOnly ? + rebase(unquotedUrl, rebaseConfig) : + URL_PREFIX + quote + rebase(unquotedUrl, rebaseConfig) + quote + URL_SUFFIX; } module.exports = rewriteUrl; diff --git a/lib/utils/apply-source-maps.js b/lib/utils/apply-source-maps.js new file mode 100644 index 00000000..e565d893 --- /dev/null +++ b/lib/utils/apply-source-maps.js @@ -0,0 +1,239 @@ +var fs = require('fs'); +var path = require('path'); + +var isAllowedResource = require('./is-allowed-resource'); +var isDataUriResource = require('./is-data-uri-resource'); +var isRemoteResource = require('./is-remote-resource'); +var loadRemoteResource = require('./load-remote-resource'); +var matchDataUri = require('./match-data-uri'); +var rebaseLocalMap = require('./rebase-local-map'); +var rebaseRemoteMap = require('./rebase-remote-map'); + +var Token = require('../tokenizer/token'); + +var MAP_MARKER_PATTERN = /^\/\*# sourceMappingURL=(\S+) \*\/$/; + +function applySourceMaps(tokens, context, callback) { + var applyContext = { + callback: callback, + index: 0, + inliner: context.options.inliner, + inputSourceMapTracker: context.inputSourceMapTracker, + localOnly: context.localOnly, + processedTokens: [], + processImportFrom: context.options.processImportFrom, + rebaseTo: context.options.rebaseTo, + sourceTokens: tokens, + warnings: context.warnings + }; + + return tokens.length > 0 ? + doApplySourceMaps(applyContext) : + callback(tokens); +} + +function doApplySourceMaps(applyContext) { + var singleSourceTokens = []; + var lastSource = findTokenSource(applyContext.sourceTokens[0]); + var source; + var token; + var l; + + for (l = applyContext.sourceTokens.length; applyContext.index < l; applyContext.index++) { + token = applyContext.sourceTokens[applyContext.index]; + source = findTokenSource(token); + + if (source != lastSource) { + singleSourceTokens = []; + lastSource = source; + } + + singleSourceTokens.push(token); + applyContext.processedTokens.push(token); + + if (token[0] == Token.COMMENT && MAP_MARKER_PATTERN.test(token[1])) { + return fetchAndApplySourceMap(token[1], source, singleSourceTokens, applyContext); + } + } + + return applyContext.callback(applyContext.processedTokens); +} + +function findTokenSource(token) { + var scope; + var metadata; + + if (token[0] == Token.AT_RULE || token[0] == Token.COMMENT) { + metadata = token[2][0]; + } else { + scope = token[1][0]; + metadata = scope[2][0]; + } + + return metadata[2]; +} + +function fetchAndApplySourceMap(sourceMapComment, source, singleSourceTokens, applyContext) { + return extractInputSourceMapFrom(sourceMapComment, applyContext, function (inputSourceMap) { + if (inputSourceMap) { + applyContext.inputSourceMapTracker.track(source, inputSourceMap); + applySourceMapRecursively(singleSourceTokens, applyContext.inputSourceMapTracker); + } + + applyContext.index++; + return doApplySourceMaps(applyContext); + }); +} + +function extractInputSourceMapFrom(sourceMapComment, applyContext, whenSourceMapReady) { + var uri = MAP_MARKER_PATTERN.exec(sourceMapComment)[1]; + var rebasedToCurrentPath; + var sourceMap; + var rebasedMap; + + if (isDataUriResource(uri)) { + sourceMap = extractInputSourceMapFromDataUri(uri); + return whenSourceMapReady(sourceMap); + } else if (isRemoteResource(uri)) { + return loadInputSourceMapFromRemoteUri(uri, applyContext, function (sourceMap) { + var parsedMap; + + if (sourceMap) { + parsedMap = JSON.parse(sourceMap); + rebasedMap = rebaseRemoteMap(parsedMap, uri); + whenSourceMapReady(rebasedMap); + } else { + whenSourceMapReady(null); + } + }); + } else { + // at this point `uri` is already rebased, see read-sources.js#rebaseSourceMapComment + // it is rebased to be consistent with rebasing other URIs + // however here we need to resolve it back to read it from disk + rebasedToCurrentPath = path.resolve(applyContext.rebaseTo, uri); + sourceMap = loadInputSourceMapFromLocalUri(rebasedToCurrentPath, applyContext); + + if (sourceMap) { + rebasedMap = rebaseLocalMap(sourceMap, rebasedToCurrentPath, applyContext.rebaseTo); + return whenSourceMapReady(rebasedMap); + } else { + return whenSourceMapReady(null); + } + } +} + +function extractInputSourceMapFromDataUri(uri) { + var dataUriMatch = matchDataUri(uri); + var charset = dataUriMatch[2] ? dataUriMatch[2].split(/[=;]/)[2] : 'us-ascii'; + var encoding = dataUriMatch[3] ? dataUriMatch[3].split(';')[1] : 'utf8'; + var data = encoding == 'utf8' ? global.unescape(dataUriMatch[4]) : dataUriMatch[4]; + + var buffer = new Buffer(data, encoding); + buffer.charset = charset; + + return JSON.parse(buffer.toString()); +} + +function loadInputSourceMapFromRemoteUri(uri, applyContext, whenLoaded) { + var isAllowed = isAllowedResource(uri, true, applyContext.processImportFrom); + + if (applyContext.localOnly) { + applyContext.warnings.push('Cannot fetch remote resource from "' + uri + '" as no callback given.'); + return whenLoaded(null); + } else if (!isAllowed) { + applyContext.warnings.push('Cannot fetch "' + uri + '" as resource is not allowed.'); + return whenLoaded(null); + } + + loadRemoteResource(uri, applyContext.inliner, function (error, body) { + if (error) { + applyContext.warnings.push('Missing source map at "' + uri + '" - ' + error); + return whenLoaded(null); + } + + whenLoaded(body); + }); +} + +function loadInputSourceMapFromLocalUri(uri, applyContext) { + var isAllowed = isAllowedResource(uri, true, applyContext.processImportFrom); + var sourceMap; + + if (!fs.existsSync(uri) || !fs.statSync(uri).isFile()) { + applyContext.warnings.push('Ignoring local source map at "' + uri + '" as resource is missing.'); + return null; + } else if (!isAllowed) { + applyContext.warnings.push('Cannot fetch "' + uri + '" as resource is not allowed.'); + return null; + } + + sourceMap = fs.readFileSync(uri, 'utf-8'); + return JSON.parse(sourceMap); +} + +function applySourceMapRecursively(tokens, inputSourceMapTracker) { + var token; + var i, l; + + for (i = 0, l = tokens.length; i < l; i++) { + token = tokens[i]; + + switch (token[0]) { + case Token.AT_RULE: + applySourceMapTo(token, inputSourceMapTracker); + break; + case Token.AT_RULE_BLOCK: + applySourceMapRecursively(token[1], inputSourceMapTracker); + applySourceMapRecursively(token[2], inputSourceMapTracker); + break; + case Token.AT_RULE_BLOCK_SCOPE: + applySourceMapTo(token, inputSourceMapTracker); + break; + case Token.BLOCK: + applySourceMapRecursively(token[1], inputSourceMapTracker); + applySourceMapRecursively(token[2], inputSourceMapTracker); + break; + case Token.BLOCK_SCOPE: + applySourceMapTo(token, inputSourceMapTracker); + break; + case Token.COMMENT: + applySourceMapTo(token, inputSourceMapTracker); + break; + case Token.PROPERTY: + applySourceMapRecursively(token, inputSourceMapTracker); + break; + case Token.PROPERTY_BLOCK: + applySourceMapRecursively(token[1], inputSourceMapTracker); + break; + case Token.PROPERTY_NAME: + applySourceMapTo(token, inputSourceMapTracker); + break; + case Token.PROPERTY_VALUE: + applySourceMapTo(token, inputSourceMapTracker); + break; + case Token.RULE: + applySourceMapRecursively(token[1], inputSourceMapTracker); + applySourceMapRecursively(token[2], inputSourceMapTracker); + break; + case Token.RULE_SCOPE: + applySourceMapTo(token, inputSourceMapTracker); + } + } + + return tokens; +} + +function applySourceMapTo(token, inputSourceMapTracker) { + var value = token[1]; + var metadata = token[2]; + var newMetadata = []; + var i, l; + + for (i = 0, l = metadata.length; i < l; i++) { + newMetadata.push(inputSourceMapTracker.originalPositionFor(metadata[i], value.length)); + } + + token[2] = newMetadata; +} + +module.exports = applySourceMaps; diff --git a/lib/utils/extract-import-url-and-media.js b/lib/utils/extract-import-url-and-media.js new file mode 100644 index 00000000..e309c2f7 --- /dev/null +++ b/lib/utils/extract-import-url-and-media.js @@ -0,0 +1,35 @@ +var split = require('../utils/split'); + +var BRACE_PREFIX = /^\(/; +var BRACE_SUFFIX = /\)$/; +var IMPORT_PREFIX_PATTERN = /^@import/i; +var QUOTE_PREFIX_PATTERN = /['"]\s*/; +var QUOTE_SUFFIX_PATTERN = /\s*['"]/; +var URL_PREFIX_PATTERN = /^url\(\s*/i; +var URL_SUFFIX_PATTERN = /\s*\)/i; + +function extractImportUrlAndMedia(atRuleValue) { + var uri; + var mediaQuery; + var stripped; + var parts; + + stripped = atRuleValue + .replace(IMPORT_PREFIX_PATTERN, '') + .trim() + .replace(URL_PREFIX_PATTERN, '(') + .replace(URL_SUFFIX_PATTERN, ')') + .replace(QUOTE_PREFIX_PATTERN, '') + .replace(QUOTE_SUFFIX_PATTERN, ''); + + parts = split(stripped, ' '); + + uri = parts[0] + .replace(BRACE_PREFIX, '') + .replace(BRACE_SUFFIX, ''); + mediaQuery = parts.slice(1).join(' '); + + return [uri, mediaQuery]; +} + +module.exports = extractImportUrlAndMedia; diff --git a/lib/utils/has-protocol.js b/lib/utils/has-protocol.js new file mode 100644 index 00000000..fa1b61fd --- /dev/null +++ b/lib/utils/has-protocol.js @@ -0,0 +1,7 @@ +var NO_PROTOCOL_RESOURCE_PATTERN = /^\/\//; + +function hasProtocol(uri) { + return !NO_PROTOCOL_RESOURCE_PATTERN.test(uri); +} + +module.exports = hasProtocol; diff --git a/lib/utils/input-source-map-tracker-2.js b/lib/utils/input-source-map-tracker-2.js deleted file mode 100644 index ff379482..00000000 --- a/lib/utils/input-source-map-tracker-2.js +++ /dev/null @@ -1,30 +0,0 @@ -var SourceMapConsumer = require('source-map').SourceMapConsumer; - -function inputSourceMapTracker() { - var maps = {}; - - return { - originalPositionFor: originalPositionFor.bind(null, maps), - track: track.bind(null, maps) - }; -} - -function originalPositionFor(maps, metadata) { - var line = metadata[0]; - var column = metadata[1]; - var source = metadata[2]; - - return source in maps ? - toMetadata(maps[source].originalPositionFor({ line: line, column: column })) : - metadata; -} - -function toMetadata(asHash) { - return [asHash.line, asHash.column, asHash.source]; -} - -function track(maps, source, data) { - maps[source] = new SourceMapConsumer(data); -} - -module.exports = inputSourceMapTracker; diff --git a/lib/utils/input-source-map-tracker.js b/lib/utils/input-source-map-tracker.js index bfd9106f..ea2c0346 100644 --- a/lib/utils/input-source-map-tracker.js +++ b/lib/utils/input-source-map-tracker.js @@ -1,284 +1,54 @@ var SourceMapConsumer = require('source-map').SourceMapConsumer; -var fs = require('fs'); -var path = require('path'); -var http = require('http'); -var https = require('https'); -var url = require('url'); - -var override = require('../utils/object.js').override; - -var MAP_MARKER = /\/\*# sourceMappingURL=(\S+) \*\//; -var REMOTE_RESOURCE = /^(https?:)?\/\//; -var DATA_URI = /^data:(\S*?)?(;charset=[^;]+)?(;[^,]+?)?,(.+)/; - -var unescape = global.unescape; - -function InputSourceMapStore(outerContext) { - this.options = outerContext.options; - this.errors = outerContext.errors; - this.warnings = outerContext.warnings; - this.sourceTracker = outerContext.sourceTracker; - this.timeout = this.options.inliner.timeout; - this.requestOptions = this.options.inliner.request; - this.localOnly = outerContext.localOnly; - this.relativeTo = outerContext.options.target || process.cwd(); - - this.maps = {}; - this.sourcesContent = {}; -} - -function fromString(self, _, whenDone) { - self.trackLoaded(undefined, undefined, self.options.sourceMap); - return whenDone(); -} - -function fromSource(self, data, whenDone, context) { - var nextAt = 0; - - function proceedToNext() { - context.cursor += nextAt + 1; - fromSource(self, data, whenDone, context); - } - - while (context.cursor < data.length) { - var fragment = data.substring(context.cursor); - - var markerStartMatch = self.sourceTracker.nextStart(fragment) || { index: -1 }; - var markerEndMatch = self.sourceTracker.nextEnd(fragment) || { index: -1 }; - var mapMatch = MAP_MARKER.exec(fragment) || { index: -1 }; - var sourceMapFile = mapMatch[1]; - - nextAt = data.length; - if (markerStartMatch.index > -1) - nextAt = markerStartMatch.index; - if (markerEndMatch.index > -1 && markerEndMatch.index < nextAt) - nextAt = markerEndMatch.index; - if (mapMatch.index > -1 && mapMatch.index < nextAt) - nextAt = mapMatch.index; - - if (nextAt == data.length) - break; - - if (nextAt == markerStartMatch.index) { - context.files.push(markerStartMatch.filename); - } else if (nextAt == markerEndMatch.index) { - context.files.pop(); - } else if (nextAt == mapMatch.index) { - var isRemote = /^https?:\/\//.test(sourceMapFile) || /^\/\//.test(sourceMapFile); - var isDataUri = DATA_URI.test(sourceMapFile); - - if (isRemote) { - return fetchMapFile(self, sourceMapFile, context, proceedToNext); - } else { - var sourceFile = context.files[context.files.length - 1]; - var sourceMapPath, sourceMapData; - var sourceDir = sourceFile ? path.dirname(sourceFile) : self.options.relativeTo; - - if (isDataUri) { - // source map's path is the same as the source file it comes from - sourceMapPath = path.resolve(self.options.root, sourceFile || ''); - sourceMapData = fromDataUri(sourceMapFile); - } else { - sourceMapPath = path.resolve(self.options.root, path.join(sourceDir || '', sourceMapFile)); - sourceMapData = fs.readFileSync(sourceMapPath, 'utf-8'); - } - self.trackLoaded(sourceFile || undefined, sourceMapPath, sourceMapData); - } - } - - context.cursor += nextAt + 1; - } - - return whenDone(); -} - -function fromDataUri(uriString) { - var match = DATA_URI.exec(uriString); - var charset = match[2] ? match[2].split(/[=;]/)[2] : 'us-ascii'; - var encoding = match[3] ? match[3].split(';')[1] : 'utf8'; - var data = encoding == 'utf8' ? unescape(match[4]) : match[4]; - - var buffer = new Buffer(data, encoding); - buffer.charset = charset; - - return buffer.toString(); +function inputSourceMapTracker() { + var maps = {}; + + return { + all: all.bind(null, maps), + isTracking: isTracking.bind(null, maps), + originalPositionFor: originalPositionFor.bind(null, maps), + track: track.bind(null, maps) + }; } -function fetchMapFile(self, sourceUrl, context, done) { - fetch(self, sourceUrl, function (data) { - self.trackLoaded(context.files[context.files.length - 1] || undefined, sourceUrl, data); - done(); - }, function (message) { - context.errors.push('Broken source map at "' + sourceUrl + '" - ' + message); - return done(); - }); +function all(maps) { + return maps; } -function fetch(self, path, onSuccess, onFailure) { - var protocol = path.indexOf('https') === 0 ? https : http; - var requestOptions = override(url.parse(path), self.requestOptions); - var errorHandled = false; - - protocol - .get(requestOptions, function (res) { - if (res.statusCode < 200 || res.statusCode > 299) - return onFailure(res.statusCode); - - var chunks = []; - res.on('data', function (chunk) { - chunks.push(chunk.toString()); - }); - res.on('end', function () { - onSuccess(chunks.join('')); - }); - }) - .on('error', function (res) { - if (errorHandled) - return; - - onFailure(res.message); - errorHandled = true; - }) - .on('timeout', function () { - if (errorHandled) - return; - - onFailure('timeout'); - errorHandled = true; - }) - .setTimeout(self.timeout); +function isTracking(maps, source) { + return source in maps; } -function originalPositionIn(trackedSource, line, column, token, allowNFallbacks) { - var originalPosition; - var maxRange = token.length; +function originalPositionFor(maps, metadata, range, selectorFallbacks) { + var line = metadata[0]; + var column = metadata[1]; + var source = metadata[2]; var position = { line: line, - column: column + maxRange + column: column + range }; + var originalPosition; - while (maxRange-- > 0) { + while (!originalPosition && position.column > column) { position.column--; - originalPosition = trackedSource.data.originalPositionFor(position); - - if (originalPosition) - break; + originalPosition = maps[source].originalPositionFor(position); } - if (originalPosition.line === null && line > 1 && allowNFallbacks > 0) - return originalPositionIn(trackedSource, line - 1, column, token, allowNFallbacks - 1); - - if (trackedSource.path && originalPosition.source) { - originalPosition.source = REMOTE_RESOURCE.test(trackedSource.path) ? - url.resolve(trackedSource.path, originalPosition.source) : - path.join(trackedSource.path, originalPosition.source); - - originalPosition.sourceResolved = true; + if (originalPosition.line === null && line > 1 && selectorFallbacks > 0) { + return originalPositionFor(maps, [line - 1, column, source], range, selectorFallbacks - 1); } - return originalPosition; + return originalPosition.line !== null ? + toMetadata(originalPosition) : + metadata; } -function trackContentSources(self, sourceFile) { - var consumer = self.maps[sourceFile].data; - var isRemote = REMOTE_RESOURCE.test(sourceFile); - var sourcesMapping = {}; - - consumer.sources.forEach(function (file, index) { - var uniquePath = isRemote ? - url.resolve(path.dirname(sourceFile), file) : - path.relative(self.relativeTo, path.resolve(path.dirname(sourceFile || '.'), file)); - - sourcesMapping[uniquePath] = consumer.sourcesContent && consumer.sourcesContent[index]; - }); - self.sourcesContent[sourceFile] = sourcesMapping; +function toMetadata(asHash) { + return [asHash.line, asHash.column, asHash.source]; } -function _resolveSources(self, remaining, whenDone) { - function processNext() { - return _resolveSources(self, remaining, whenDone); - } - - if (remaining.length === 0) - return whenDone(); - - var current = remaining.shift(); - var sourceFile = current[0]; - var originalFile = current[1]; - var isRemote = REMOTE_RESOURCE.test(sourceFile); - - if (isRemote && self.localOnly) { - self.warnings.push('No callback given to `#minify` method, cannot fetch a remote file from "' + originalFile + '"'); - return processNext(); - } - - if (isRemote) { - fetch(self, originalFile, function (data) { - self.sourcesContent[sourceFile][originalFile] = data; - processNext(); - }, function (message) { - self.warnings.push('Broken original source file at "' + originalFile + '" - ' + message); - processNext(); - }); - } else { - var fullPath = path.join(self.options.root, originalFile); - if (fs.existsSync(fullPath)) - self.sourcesContent[sourceFile][originalFile] = fs.readFileSync(fullPath, 'utf-8'); - else - self.warnings.push('Missing original source file at "' + fullPath + '".'); - return processNext(); - } +function track(maps, source, data) { + maps[source] = new SourceMapConsumer(data); } -InputSourceMapStore.prototype.track = function (data, whenDone) { - return typeof this.options.sourceMap == 'string' ? - fromString(this, data, whenDone) : - fromSource(this, data, whenDone, { files: [], cursor: 0, errors: this.errors }); -}; - -InputSourceMapStore.prototype.trackLoaded = function (sourcePath, mapPath, mapData) { - var relativeTo = this.options.explicitTarget ? this.options.target : this.options.root; - var isRemote = REMOTE_RESOURCE.test(sourcePath); - - if (mapPath) { - mapPath = isRemote ? - path.dirname(mapPath) : - path.dirname(path.relative(relativeTo, mapPath)); - } - - this.maps[sourcePath] = { - path: mapPath, - data: new SourceMapConsumer(mapData) - }; - - trackContentSources(this, sourcePath); -}; - -InputSourceMapStore.prototype.isTracking = function (source) { - return !!this.maps[source]; -}; - -InputSourceMapStore.prototype.originalPositionFor = function (sourceInfo, token, allowNFallbacks) { - return originalPositionIn(this.maps[sourceInfo.source], sourceInfo.line, sourceInfo.column, token, allowNFallbacks); -}; - -InputSourceMapStore.prototype.sourcesContentFor = function (contextSource) { - return this.sourcesContent[contextSource]; -}; - -InputSourceMapStore.prototype.resolveSources = function (whenDone) { - var toResolve = []; - - for (var sourceFile in this.sourcesContent) { - var contents = this.sourcesContent[sourceFile]; - for (var originalFile in contents) { - if (!contents[originalFile]) - toResolve.push([sourceFile, originalFile]); - } - } - - return _resolveSources(this, toResolve, whenDone); -}; - -module.exports = InputSourceMapStore; +module.exports = inputSourceMapTracker; diff --git a/lib/utils/is-absolute-resource.js b/lib/utils/is-absolute-resource.js new file mode 100644 index 00000000..baa80231 --- /dev/null +++ b/lib/utils/is-absolute-resource.js @@ -0,0 +1,7 @@ +var isRemoteResource = require('./is-remote-resource'); + +function isAbsoluteResource(uri) { + return !isRemoteResource(uri) && uri[0] == '/'; +} + +module.exports = isAbsoluteResource; diff --git a/lib/utils/is-allowed-resource.js b/lib/utils/is-allowed-resource.js new file mode 100644 index 00000000..d2797192 --- /dev/null +++ b/lib/utils/is-allowed-resource.js @@ -0,0 +1,46 @@ +var url = require('url'); + +var hasProtocol = require('./has-protocol'); + +var HTTP_PROTOCOL = 'http:'; + +function isAllowedResource(uri, isRemote, rules) { + var match; + var allowed = true; + var rule; + var i; + + if (rules.length === 0) { + return false; + } + + if (isRemote && !hasProtocol(uri)) { + uri = HTTP_PROTOCOL + uri; + } + + match = isRemote ? + url.parse(uri).host : + uri; + + for (i = 0; i < rules.length; i++) { + rule = rules[i]; + + if (rule == 'all') { + allowed = true; + } else if (isRemote && rule == 'local') { + allowed = false; + } else if (isRemote && rule == 'remote') { + allowed = true; + } else if (!isRemote && rule == 'remote') { + allowed = false; + } else if (!isRemote && rule == 'local') { + allowed = true; + } else if (rule[0] == '!' && rule.substring(1) === match) { + allowed = false; + } + } + + return allowed; +} + +module.exports = isAllowedResource; diff --git a/lib/utils/is-data-uri-resource.js b/lib/utils/is-data-uri-resource.js new file mode 100644 index 00000000..58558110 --- /dev/null +++ b/lib/utils/is-data-uri-resource.js @@ -0,0 +1,7 @@ +var DATA_URI_PATTERN = /^data:(\S*?)?(;charset=[^;]+)?(;[^,]+?)?,(.+)/; + +function isDataUriResource(uri) { + return DATA_URI_PATTERN.test(uri); +} + +module.exports = isDataUriResource; diff --git a/lib/utils/is-http-resource.js b/lib/utils/is-http-resource.js new file mode 100644 index 00000000..5179c2ea --- /dev/null +++ b/lib/utils/is-http-resource.js @@ -0,0 +1,7 @@ +var HTTP_RESOURCE_PATTERN = /^http:\/\//; + +function isHttpResource(uri) { + return HTTP_RESOURCE_PATTERN.test(uri); +} + +module.exports = isHttpResource; diff --git a/lib/utils/is-https-resource.js b/lib/utils/is-https-resource.js new file mode 100644 index 00000000..c6938f57 --- /dev/null +++ b/lib/utils/is-https-resource.js @@ -0,0 +1,7 @@ +var HTTPS_RESOURCE_PATTERN = /^https:\/\//; + +function isHttpsResource(uri) { + return HTTPS_RESOURCE_PATTERN.test(uri); +} + +module.exports = isHttpsResource; diff --git a/lib/utils/is-import.js b/lib/utils/is-import.js new file mode 100644 index 00000000..72abc328 --- /dev/null +++ b/lib/utils/is-import.js @@ -0,0 +1,7 @@ +var IMPORT_PREFIX_PATTERN = /^@import/i; + +function isImport(value) { + return IMPORT_PREFIX_PATTERN.test(value); +} + +module.exports = isImport; diff --git a/lib/utils/is-remote-resource.js b/lib/utils/is-remote-resource.js new file mode 100644 index 00000000..fb3b61f3 --- /dev/null +++ b/lib/utils/is-remote-resource.js @@ -0,0 +1,7 @@ +var REMOTE_RESOURCE_PATTERN = /^(\w+:\/\/|\/\/)/; + +function isRemoteResource(uri) { + return REMOTE_RESOURCE_PATTERN.test(uri); +} + +module.exports = isRemoteResource; diff --git a/lib/utils/load-original-sources.js b/lib/utils/load-original-sources.js new file mode 100644 index 00000000..ba661998 --- /dev/null +++ b/lib/utils/load-original-sources.js @@ -0,0 +1,117 @@ +var fs = require('fs'); +var path = require('path'); + +var isAllowedResource = require('./is-allowed-resource'); +var isRemoteResource = require('./is-remote-resource'); +var loadRemoteResource = require('./load-remote-resource'); + +function loadOriginalSources(context, callback) { + var loadContext = { + callback: callback, + index: 0, + inliner: context.options.inliner, + localOnly: context.localOnly, + processImportFrom: context.options.processImportFrom, + rebaseTo: context.options.rebaseTo, + sourcesContent: context.sourcesContent, + uriToSource: uriToSourceMapping(context.inputSourceMapTracker.all()), + warnings: context.warnings + }; + + return doLoadOriginalSources(loadContext); +} + +function uriToSourceMapping(allSourceMapConsumers) { + var mapping = {}; + var consumer; + var uri; + var source; + var i, l; + + for (source in allSourceMapConsumers) { + consumer = allSourceMapConsumers[source]; + + for (i = 0, l = consumer.sources.length; i < l; i++) { + uri = consumer.sources[i]; + source = consumer.sourceContentFor(uri, true); + + mapping[uri] = source; + } + } + + return mapping; +} + +function doLoadOriginalSources(loadContext) { + var uris = Object.keys(loadContext.uriToSource); + var uri; + var source; + var total; + + for (total = uris.length; loadContext.index < total; loadContext.index++) { + uri = uris[loadContext.index]; + source = loadContext.uriToSource[uri]; + + if (source) { + loadContext.sourcesContent[uri] = source; + } else { + return loadOriginalSource(uri, loadContext); + } + } + + return loadContext.callback(); +} + +function loadOriginalSource(uri, loadContext) { + var content; + + if (isRemoteResource(uri)) { + return loadOriginalSourceFromRemoteUri(uri, loadContext, function (content) { + loadContext.index++; + loadContext.sourcesContent[uri] = content; + return doLoadOriginalSources(loadContext); + }); + } else { + content = loadOriginalSourceFromLocalUri(uri, loadContext); + loadContext.index++; + loadContext.sourcesContent[uri] = content; + return doLoadOriginalSources(loadContext); + } +} + +function loadOriginalSourceFromRemoteUri(uri, loadContext, whenLoaded) { + var isAllowed = isAllowedResource(uri, true, loadContext.processImportFrom); + + if (loadContext.localOnly) { + loadContext.warnings.push('Cannot fetch remote resource from "' + uri + '" as no callback given.'); + return whenLoaded(null); + } else if (!isAllowed) { + loadContext.warnings.push('Cannot fetch "' + uri + '" as resource is not allowed.'); + return whenLoaded(null); + } + + loadRemoteResource(uri, loadContext.inliner, function (error, content) { + if (error) { + loadContext.warnings.push('Missing original source at "' + uri + '" - ' + error); + } + + whenLoaded(content); + }); +} + +function loadOriginalSourceFromLocalUri(uri, loadContext) { + var isAllowed = isAllowedResource(uri, true, loadContext.processImportFrom); + var resolvedUri = path.resolve(loadContext.rebaseTo, uri); + + if (!fs.existsSync(resolvedUri) || !fs.statSync(resolvedUri).isFile()) { + loadContext.warnings.push('Ignoring local source map at "' + resolvedUri + '" as resource is missing.'); + return null; + } else if (!isAllowed) { + loadContext.warnings.push('Cannot fetch "' + resolvedUri + '" as resource is not allowed.'); + return null; + } + + return fs.readFileSync(resolvedUri, 'utf8'); +} + +module.exports = loadOriginalSources; diff --git a/lib/utils/load-remote-resource.js b/lib/utils/load-remote-resource.js new file mode 100644 index 00000000..97605749 --- /dev/null +++ b/lib/utils/load-remote-resource.js @@ -0,0 +1,75 @@ +var http = require('http'); +var https = require('https'); +var url = require('url'); + +var hasProtocol = require('./has-protocol'); +var isHttpResource = require('./is-http-resource'); +var isHttpsResource = require('./is-https-resource'); +var override = require('./override'); + +var HTTP_PROTOCOL = 'http:'; + +function loadRemoteResource(uri, inlinerOptions, callback) { + var proxyProtocol = inlinerOptions.request.protocol || inlinerOptions.request.hostname; + var errorHandled = false; + var requestOptions; + var fetch; + + if (!hasProtocol(uri)) { + uri = 'http:' + uri; + } + + requestOptions = override( + url.parse(uri), + inlinerOptions.request || {} + ); + + if (inlinerOptions.request.hostname !== undefined) { + // overwrite as we always expect a http proxy currently + requestOptions.protocol = inlinerOptions.request.protocol || HTTP_PROTOCOL; + requestOptions.path = requestOptions.href; + } + + fetch = (proxyProtocol && !isHttpsResource(proxyProtocol)) || isHttpResource(uri) ? + http.get : + https.get; + + fetch(requestOptions, function (res) { + var chunks = []; + var movedUri; + + if (res.statusCode < 200 || res.statusCode > 399) { + return callback(res.statusCode, null); + } else if (res.statusCode > 299) { + movedUri = url.resolve(uri, res.headers.location); + return loadRemoteResource(movedUri, inlinerOptions, callback); + } + + res.on('data', function (chunk) { + chunks.push(chunk.toString()); + }); + res.on('end', function () { + var body = chunks.join(''); + callback(null, body); + }); + }) + .on('error', function (res) { + if (errorHandled) { + return; + } + + errorHandled = true; + callback(res.message, null); + }) + .on('timeout', function () { + if (errorHandled) { + return; + } + + errorHandled = true; + callback('timeout', null); + }) + .setTimeout(inlinerOptions.timeout); +} + +module.exports = loadRemoteResource; diff --git a/lib/utils/match-data-uri.js b/lib/utils/match-data-uri.js new file mode 100644 index 00000000..d0d5a4c7 --- /dev/null +++ b/lib/utils/match-data-uri.js @@ -0,0 +1,7 @@ +var DATA_URI_PATTERN = /^data:(\S*?)?(;charset=[^;]+)?(;[^,]+?)?,(.+)/; + +function matchDataUri(uri) { + return DATA_URI_PATTERN.exec(uri); +} + +module.exports = matchDataUri; diff --git a/lib/utils/read-sources.js b/lib/utils/read-sources.js index 24da9dd4..418ed6e6 100644 --- a/lib/utils/read-sources.js +++ b/lib/utils/read-sources.js @@ -1,32 +1,35 @@ var fs = require('fs'); -var http = require('http'); -var https = require('https'); var path = require('path'); -var url = require('url'); + +var applySourceMaps = require('./apply-source-maps'); +var extractImportUrlAndMedia = require('./extract-import-url-and-media'); +var isAbsoluteResource = require('./is-absolute-resource'); +var isAllowedResource = require('./is-allowed-resource'); +var isImport = require('./is-import'); +var isRemoteResource = require('./is-remote-resource'); +var loadOriginalSources = require('./load-original-sources'); +var loadRemoteResource = require('./load-remote-resource'); +var rebase = require('./rebase'); +var rebaseLocalMap = require('./rebase-local-map'); +var rebaseRemoteMap = require('./rebase-remote-map'); +var restoreImport = require('./restore-import'); var tokenize = require('../tokenizer/tokenize'); var Token = require('../tokenizer/token'); -var rewriteUrl = require('../urls/rewrite'); - -var override = require('../utils/override'); -var split = require('../utils/split'); - -var IMPORT_PREFIX_PATTERN = /^@import/i; -var BRACE_PREFIX = /^\(/; -var BRACE_SUFFIX = /\)$/; -var QUOTE_PREFIX_PATTERN = /['"]\s*/; -var QUOTE_SUFFIX_PATTERN = /\s*['"]/; -var URL_PREFIX_PATTERN = /^url\(\s*/i; -var URL_SUFFIX_PATTERN = /\s*\)/i; - -var HTTP_PROTOCOL = 'http:'; -var HTTP_RESOURCE_PATTERN = /^http:\/\//; -var HTTPS_RESOURCE_PATTERN = /^https:\/\//; -var NO_PROTOCOL_RESOURCE_PATTERN = /^\/\//; -var REMOTE_RESOURCE_PATTERN = /^(https?:)?\/\//; +var UNKNOWN_SOURCE = 'unknown-source'; function readSources(input, context, callback) { + return doReadSources(input, context, function (tokens) { + return applySourceMaps(tokens, context, function () { + return context.options.sourceMapInlineSources ? + loadOriginalSources(context, function () { return callback(tokens); }) : + callback(tokens); + }); + }); +} + +function doReadSources(input, context, callback) { if (typeof input == 'string') { return fromString(input, context, {}, callback); } else if (Buffer.isBuffer(input)) { @@ -41,23 +44,32 @@ function readSources(input, context, callback) { function fromString(input, context, parentInlinerContext, callback) { var inputAsHash = {}; - inputAsHash[false] = { - styles: input + inputAsHash[UNKNOWN_SOURCE] = { + styles: input, + sourceMap: (typeof context.options.sourceMap === 'string') ? context.options.sourceMap : null }; return fromHash(inputAsHash, context, parentInlinerContext, callback); } function fromArray(input, context, parentInlinerContext, callback) { + var currentPath = path.resolve(''); var inputAsHash = input.reduce(function (accumulator, uri) { - var absolutePath = uri[0] == '/' ? + var isRemoteUri = isRemoteResource(uri); + var absolutePath = uri[0] == '/' || isRemoteUri ? uri : path.resolve(uri); + var relativeToCurrentPath; - if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) { - context.errors.push('Ignoring "' + uri + '" as resource is missing.'); - } else { + if (isRemoteUri) { accumulator[uri] = { + styles: restoreImport(uri, '') + ';' + }; + } else if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) { + context.errors.push('Ignoring "' + absolutePath + '" as resource is missing.'); + } else { + relativeToCurrentPath = path.relative(currentPath, absolutePath); + accumulator[relativeToCurrentPath] = { styles: fs.readFileSync(absolutePath, 'utf-8') }; } @@ -70,25 +82,28 @@ function fromArray(input, context, parentInlinerContext, callback) { function fromHash(input, context, parentInlinerContext, callback) { var tokens = []; + var newTokens = []; var sourcePath; var source; var rebaseFrom; var rebaseTo; + var parsedMap; + var rebasedMap; var rebaseConfig; for (sourcePath in input) { source = input[sourcePath]; - if (isRemote(sourcePath)) { + if (sourcePath !== UNKNOWN_SOURCE && isRemoteResource(sourcePath)) { rebaseFrom = sourcePath; rebaseTo = sourcePath; - } else if (isAbsolute(sourcePath)) { + } else if (sourcePath !== UNKNOWN_SOURCE && isAbsoluteResource(sourcePath)) { rebaseFrom = path.dirname(sourcePath); rebaseTo = context.options.rebaseTo; } else { - rebaseFrom = sourcePath ? + rebaseFrom = sourcePath !== UNKNOWN_SOURCE ? path.dirname(path.resolve(sourcePath)) : - path.resolve(sourcePath); + path.resolve(''); rebaseTo = context.options.rebaseTo; } @@ -97,123 +112,45 @@ function fromHash(input, context, parentInlinerContext, callback) { toBase: rebaseTo }; - tokens = tokens.concat( - rebase( - tokenize(source.styles, context), - context.options.rebase, - context.validator, - rebaseConfig - ) - ); - - context.stats.originalSize += source.styles.length; - } - - return context.options.processImport ? - inlineImports(tokens, context, parentInlinerContext, callback) : - callback(tokens); -} - -function isAbsolute(uri) { - return uri && uri[0] == '/'; -} - -function rebase(tokens, rebaseAll, validator, rebaseConfig) { - return rebaseAll ? - rebaseEverything(tokens, validator, rebaseConfig) : - rebaseAtRules(tokens, validator, rebaseConfig); -} - -function rebaseEverything(tokens, validator, rebaseConfig) { - var token; - var i, l; - - for (i = 0, l = tokens.length; i < l; i++) { - token = tokens[i]; - - switch (token[0]) { - case Token.AT_RULE: - rebaseAtRule(token, validator, rebaseConfig); - break; - case Token.AT_RULE_BLOCK: - // - break; - case Token.BLOCK: - rebaseEverything(token[2], validator, rebaseConfig); - break; - case Token.PROPERTY: - // - break; - case Token.RULE: - rebaseProperties(token[2], validator, rebaseConfig); - break; + if (source.sourceMap) { + parsedMap = JSON.parse(source.sourceMap); + rebasedMap = isRemoteResource(sourcePath) ? + rebaseRemoteMap(parsedMap, sourcePath) : + rebaseLocalMap(parsedMap, sourcePath, context.options.rebaseTo); + context.inputSourceMapTracker.track(sourcePath, rebasedMap); } - } - - return tokens; -} - -function rebaseAtRules(tokens, validator, rebaseConfig) { - var token; - var i, l; - for (i = 0, l = tokens.length; i < l; i++) { - token = tokens[i]; + context.source = sourcePath !== UNKNOWN_SOURCE ? sourcePath : undefined; + context.sourcesContent[context.source] = source.styles; - switch (token[0]) { - case Token.AT_RULE: - rebaseAtRule(token, validator, rebaseConfig); - break; - } - } + newTokens = tokenize(source.styles, context); + newTokens = rebase(newTokens, context.options.rebase, context.validator, rebaseConfig); - return tokens; -} + tokens = tokens.concat(newTokens); -function rebaseAtRule(token, validator, rebaseConfig) { - if (!IMPORT_PREFIX_PATTERN.test(token[1])) { - return; + context.stats.originalSize += source.styles.length; } - var uriAndMediaQuery = extractUrlAndMedia(token[1]); - var newUrl = rewriteUrl(uriAndMediaQuery[0], rebaseConfig); - var mediaQuery = uriAndMediaQuery[1]; - - token[1] = restoreImport(newUrl, mediaQuery); -} - -function restoreImport(uri, mediaQuery) { - return ('@import ' + uri + ' ' + mediaQuery).trim(); -} - -function rebaseProperties(properties, validator, rebaseConfig) { - var property; - var value; - var i, l; - var j, m; - - for (i = 0, l = properties.length; i < l; i++) { - property = properties[i]; - - for (j = 2 /* 0 is Token.PROPERTY, 1 is name */, m = property.length; j < m; j++) { - value = property[j][1]; - - if (validator.isValidUrl(value)) { - property[j][1] = rewriteUrl(value, rebaseConfig); - } - } - } + return context.options.processImport ? + inlineImports(tokens, context, parentInlinerContext, callback) : + callback(tokens); } function inlineImports(tokens, externalContext, parentInlinerContext, callback) { var inlinerContext = { afterContent: false, callback: callback, + errors: externalContext.errors, externalContext: externalContext, imported: parentInlinerContext.imported || [], + inlinerOptions: externalContext.options.inliner, isRemote: parentInlinerContext.isRemote || false, + localOnly: externalContext.localOnly, outputTokens: [], - sourceTokens: tokens + processImportFrom: externalContext.options.processImportFrom, + rebaseTo: externalContext.options.rebaseTo, + sourceTokens: tokens, + warnings: externalContext.warnings }; return doInlineImports(inlinerContext); @@ -226,7 +163,7 @@ function doInlineImports(inlinerContext) { for (i = 0, l = inlinerContext.sourceTokens.length; i < l; i++) { token = inlinerContext.sourceTokens[i]; - if (token[0] == Token.AT_RULE && IMPORT_PREFIX_PATTERN.test(token[1])) { + if (token[0] == Token.AT_RULE && isImport(token[1])) { inlinerContext.sourceTokens.splice(0, i); return inlineStylesheet(token, inlinerContext); } else if (token[0] == Token.AT_RULE || token[0] == Token.COMMENT) { @@ -242,220 +179,109 @@ function doInlineImports(inlinerContext) { } function inlineStylesheet(token, inlinerContext) { - var uriAndMediaQuery = extractUrlAndMedia(token[1]); + var uriAndMediaQuery = extractImportUrlAndMedia(token[1]); var uri = uriAndMediaQuery[0]; var mediaQuery = uriAndMediaQuery[1]; + var metadata = token[2]; - return isRemote(uri) ? - inlineRemoteStylesheet(uri, mediaQuery, inlinerContext) : - inlineLocalStylesheet(uri, mediaQuery, inlinerContext); -} - -function extractUrlAndMedia(atRuleValue) { - var uri; - var mediaQuery; - var stripped; - var parts; - - stripped = atRuleValue - .replace(IMPORT_PREFIX_PATTERN, '') - .trim() - .replace(URL_PREFIX_PATTERN, '(') - .replace(URL_SUFFIX_PATTERN, ')') - .replace(QUOTE_PREFIX_PATTERN, '') - .replace(QUOTE_SUFFIX_PATTERN, ''); - - parts = split(stripped, ' '); - - uri = parts[0] - .replace(BRACE_PREFIX, '') - .replace(BRACE_SUFFIX, ''); - mediaQuery = parts.slice(1).join(' '); - - return [uri, mediaQuery]; -} - -function isRemote(uri) { - return uri && REMOTE_RESOURCE_PATTERN.test(uri); -} - -function allowedResource(uri, isRemote, rules) { - var match; - var allowed = true; - var rule; - var i; - - if (rules.length === 0) { - return false; - } - - if (isRemote && NO_PROTOCOL_RESOURCE_PATTERN.test(uri)) { - uri = 'http:' + uri; - } - - match = isRemote ? - url.parse(uri).host : - uri; - - for (i = 0; i < rules.length; i++) { - rule = rules[i]; - - if (rule == 'all') { - allowed = true; - } else if (isRemote && rule == 'local') { - allowed = false; - } else if (isRemote && rule == 'remote') { - allowed = true; - } else if (!isRemote && rule == 'remote') { - allowed = false; - } else if (!isRemote && rule == 'local') { - allowed = true; - } else if (rule[0] == '!' && rule.substring(1) === match) { - allowed = false; - } - } - - return allowed; + return isRemoteResource(uri) ? + inlineRemoteStylesheet(uri, mediaQuery, metadata, inlinerContext) : + inlineLocalStylesheet(uri, mediaQuery, metadata, inlinerContext); } -function inlineRemoteStylesheet(uri, mediaQuery, inlinerContext) { - var inliner = inlinerContext.externalContext.options.inliner; - var errorHandled = false; - var fetch; - var isAllowed = allowedResource(uri, true, inlinerContext.externalContext.options.processImportFrom); - var onError; - var options; +function inlineRemoteStylesheet(uri, mediaQuery, metadata, inlinerContext) { + var isAllowed = isAllowedResource(uri, true, inlinerContext.processImportFrom); var originalUri = uri; - var proxyProtocol = inliner.request.protocol || inliner.request.hostname; if (inlinerContext.imported.indexOf(uri) > -1) { - inlinerContext.externalContext.warnings.push('Ignoring remote @import of "' + uri + '" as it has already been imported.'); + inlinerContext.warnings.push('Ignoring remote @import of "' + uri + '" as it has already been imported.'); inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1); return doInlineImports(inlinerContext); - } else if (inlinerContext.externalContext.localOnly && inlinerContext.afterContent) { - inlinerContext.externalContext.warnings.push('Ignoring remote @import of "' + uri + '" as no callback given and after other content.'); + } else if (inlinerContext.localOnly && inlinerContext.afterContent) { + inlinerContext.warnings.push('Ignoring remote @import of "' + uri + '" as no callback given and after other content.'); inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1); return doInlineImports(inlinerContext); - } else if (inlinerContext.externalContext.localOnly) { - inlinerContext.externalContext.warnings.push('Skipping remote @import of "' + uri + '" as no callback given.'); + } else if (inlinerContext.localOnly) { + inlinerContext.warnings.push('Skipping remote @import of "' + uri + '" as no callback given.'); inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1)); inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1); return doInlineImports(inlinerContext); } else if (!isAllowed && inlinerContext.afterContent) { - inlinerContext.externalContext.warnings.push('Ignoring remote @import of "' + uri + '" as resource not allowed and after other content.'); + inlinerContext.warnings.push('Ignoring remote @import of "' + uri + '" as resource is not allowed and after other content.'); inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1); return doInlineImports(inlinerContext); } else if (!isAllowed) { - inlinerContext.externalContext.warnings.push('Skipping remote @import of "' + uri + '" as resource not allowed.'); + inlinerContext.warnings.push('Skipping remote @import of "' + uri + '" as resource is not allowed.'); inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1)); inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1); return doInlineImports(inlinerContext); } - if (NO_PROTOCOL_RESOURCE_PATTERN.test(uri)) { - uri = 'http:' + uri; - } - - fetch = (proxyProtocol && !HTTPS_RESOURCE_PATTERN.test(proxyProtocol)) || HTTP_RESOURCE_PATTERN.test(uri) ? - http.get : - https.get; - - options = override(url.parse(uri), inliner.request); - if (inliner.request.hostname !== undefined) { - // overwrite as we always expect a http proxy currently - options.protocol = inliner.request.protocol || HTTP_PROTOCOL; - options.path = options.href; - } - - onError = function(message) { - if (errorHandled) - return; - - errorHandled = true; - inlinerContext.externalContext.errors.push('Broken @import declaration of "' + uri + '" - ' + message); - - process.nextTick(function () { - inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1)); - inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1); - doInlineImports(inlinerContext); - }); - }; - inlinerContext.imported.push(uri); - fetch(options, function (res) { - var chunks = []; - var movedUri; + loadRemoteResource(uri, inlinerContext.inlinerOptions, function (error, importedStyles) { + var sourceHash = {}; - if (res.statusCode < 200 || res.statusCode > 399) { - return onError('error ' + res.statusCode); - } else if (res.statusCode > 299) { - movedUri = url.resolve(uri, res.headers.location); - return inlineRemoteStylesheet(movedUri, mediaQuery, inlinerContext); - } + if (error) { + inlinerContext.errors.push('Broken @import declaration of "' + uri + '" - ' + error); - res.on('data', function (chunk) { - chunks.push(chunk.toString()); - }); - res.on('end', function () { - var importedStyles; - var sourceHash = {}; + return process.nextTick(function () { + inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1)); + inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1); + doInlineImports(inlinerContext); + }); + } - importedStyles = chunks.join(''); - sourceHash[originalUri] = { - styles: importedStyles - }; + sourceHash[originalUri] = { + styles: importedStyles + }; - inlinerContext.isRemote = true; - fromHash(sourceHash, inlinerContext.externalContext, inlinerContext, function (importedTokens) { - importedTokens = wrapInMedia(importedTokens, mediaQuery); + inlinerContext.isRemote = true; + fromHash(sourceHash, inlinerContext.externalContext, inlinerContext, function (importedTokens) { + importedTokens = wrapInMedia(importedTokens, mediaQuery, metadata); - inlinerContext.outputTokens = inlinerContext.outputTokens.concat(importedTokens); - inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1); + inlinerContext.outputTokens = inlinerContext.outputTokens.concat(importedTokens); + inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1); - doInlineImports(inlinerContext); - }); + doInlineImports(inlinerContext); }); - - }) - .on('error', function (res) { - onError(res.message); - }) - .on('timeout', function () { - onError('timeout'); - }) - .setTimeout(inliner.timeout); + }); } -function inlineLocalStylesheet(uri, mediaQuery, inlinerContext) { - var resolvedPath = uri[0] == '/' ? - path.resolve(path.resolve(''), uri.substring(1)) : - path.resolve(inlinerContext.externalContext.options.rebaseTo, uri); +function inlineLocalStylesheet(uri, mediaQuery, metadata, inlinerContext) { + var currentPath = path.resolve(''); + var relativeTo = uri[0] == '/' ? + currentPath : + path.resolve(inlinerContext.rebaseTo || ''); + var absolutePath = uri[0] == '/' ? + path.resolve(relativeTo, uri.substring(1)) : + path.resolve(relativeTo, uri); + var relativeToCurrentPath = path.relative(currentPath, absolutePath); var importedStyles; var importedTokens; - var isAllowed = allowedResource(uri, false, inlinerContext.externalContext.options.processImportFrom); + var isAllowed = isAllowedResource(uri, false, inlinerContext.processImportFrom); var sourceHash = {}; - if (inlinerContext.imported.indexOf(resolvedPath) > -1) { - inlinerContext.externalContext.warnings.push('Ignoring local @import of "' + uri + '" as it has already beeb imported.'); - } else if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isFile()) { - inlinerContext.externalContext.errors.push('Ignoring local @import of "' + uri + '" as resource is missing.'); + if (inlinerContext.imported.indexOf(absolutePath) > -1) { + inlinerContext.warnings.push('Ignoring local @import of "' + uri + '" as it has already been imported.'); + } else if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) { + inlinerContext.errors.push('Ignoring local @import of "' + uri + '" as resource is missing.'); } else if (!isAllowed && inlinerContext.afterContent) { - inlinerContext.externalContext.warnings.push('Ignoring local @import of "' + uri + '" as resource not allowed and after other content.'); + inlinerContext.warnings.push('Ignoring local @import of "' + uri + '" as resource is not allowed and after other content.'); } else if (inlinerContext.afterContent) { - inlinerContext.externalContext.warnings.push('Ignoring local @import of "' + uri + '" as after other content.'); + inlinerContext.warnings.push('Ignoring local @import of "' + uri + '" as after other content.'); } else if (!isAllowed) { - inlinerContext.externalContext.warnings.push('Skipping local @import of "' + uri + '" as resource not allowed.'); + inlinerContext.warnings.push('Skipping local @import of "' + uri + '" as resource is not allowed.'); inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1)); } else { - importedStyles = fs.readFileSync(resolvedPath, 'utf-8'); - inlinerContext.imported.push(resolvedPath); + importedStyles = fs.readFileSync(absolutePath, 'utf-8'); + inlinerContext.imported.push(absolutePath); - sourceHash[resolvedPath] = { + sourceHash[relativeToCurrentPath] = { styles: importedStyles }; importedTokens = fromHash(sourceHash, inlinerContext.externalContext, inlinerContext, function (tokens) { return tokens; }); - importedTokens = wrapInMedia(importedTokens, mediaQuery); + importedTokens = wrapInMedia(importedTokens, mediaQuery, metadata); inlinerContext.outputTokens = inlinerContext.outputTokens.concat(importedTokens); } @@ -465,9 +291,9 @@ function inlineLocalStylesheet(uri, mediaQuery, inlinerContext) { return doInlineImports(inlinerContext); } -function wrapInMedia(tokens, mediaQuery) { +function wrapInMedia(tokens, mediaQuery, metadata) { if (mediaQuery) { - return [[Token.BLOCK, [[Token.BLOCK_SCOPE, '@media ' + mediaQuery]], tokens]]; + return [[Token.BLOCK, [[Token.BLOCK_SCOPE, '@media ' + mediaQuery, metadata]], tokens]]; } else { return tokens; } diff --git a/lib/utils/rebase-local-map.js b/lib/utils/rebase-local-map.js new file mode 100644 index 00000000..3ca288d4 --- /dev/null +++ b/lib/utils/rebase-local-map.js @@ -0,0 +1,15 @@ +var path = require('path'); + +function rebaseLocalMap(sourceMap, sourceUri, rebaseTo) { + var currentPath = path.resolve(''); + var absolutePath = path.resolve(currentPath, sourceUri); + var sourceDirectory = path.dirname(absolutePath); + + sourceMap.sources = sourceMap.sources.map(function(source) { + return path.relative(rebaseTo, path.resolve(sourceDirectory, source)); + }); + + return sourceMap; +} + +module.exports = rebaseLocalMap; diff --git a/lib/utils/rebase-remote-map.js b/lib/utils/rebase-remote-map.js new file mode 100644 index 00000000..7b6bb7ac --- /dev/null +++ b/lib/utils/rebase-remote-map.js @@ -0,0 +1,14 @@ +var path = require('path'); +var url = require('url'); + +function rebaseRemoteMap(sourceMap, sourceUri) { + var sourceDirectory = path.dirname(sourceUri); + + sourceMap.sources = sourceMap.sources.map(function(source) { + return url.resolve(sourceDirectory, source); + }); + + return sourceMap; +} + +module.exports = rebaseRemoteMap; diff --git a/lib/utils/rebase.js b/lib/utils/rebase.js new file mode 100644 index 00000000..78487970 --- /dev/null +++ b/lib/utils/rebase.js @@ -0,0 +1,101 @@ +var extractImportUrlAndMedia = require('./extract-import-url-and-media'); +var isImport = require('./is-import'); +var restoreImport = require('./restore-import'); + +var rewriteUrl = require('../urls/rewrite'); +var Token = require('../tokenizer/token'); + +var SOURCE_MAP_COMMENT_PATTERN = /^\/\*# sourceMappingURL=(\S+) \*\/$/; + +function rebase(tokens, rebaseAll, validator, rebaseConfig) { + return rebaseAll ? + rebaseEverything(tokens, validator, rebaseConfig) : + rebaseAtRules(tokens, validator, rebaseConfig); +} + +function rebaseEverything(tokens, validator, rebaseConfig) { + var token; + var i, l; + + for (i = 0, l = tokens.length; i < l; i++) { + token = tokens[i]; + + switch (token[0]) { + case Token.AT_RULE: + rebaseAtRule(token, validator, rebaseConfig); + break; + case Token.AT_RULE_BLOCK: + rebaseProperties(token[2], validator, rebaseConfig); + break; + case Token.BLOCK: + rebaseEverything(token[2], validator, rebaseConfig); + break; + case Token.COMMENT: + rebaseSourceMapComment(token, rebaseConfig); + break; + case Token.RULE: + rebaseProperties(token[2], validator, rebaseConfig); + break; + } + } + + return tokens; +} + +function rebaseAtRules(tokens, validator, rebaseConfig) { + var token; + var i, l; + + for (i = 0, l = tokens.length; i < l; i++) { + token = tokens[i]; + + switch (token[0]) { + case Token.AT_RULE: + rebaseAtRule(token, validator, rebaseConfig); + break; + } + } + + return tokens; +} + +function rebaseAtRule(token, validator, rebaseConfig) { + if (!isImport(token[1])) { + return; + } + + var uriAndMediaQuery = extractImportUrlAndMedia(token[1]); + var newUrl = rewriteUrl(uriAndMediaQuery[0], rebaseConfig); + var mediaQuery = uriAndMediaQuery[1]; + + token[1] = restoreImport(newUrl, mediaQuery); +} + +function rebaseSourceMapComment(token, rebaseConfig) { + var matches = SOURCE_MAP_COMMENT_PATTERN.exec(token[1]); + + if (matches && matches[1].indexOf('data:') === -1) { + token[1] = token[1].replace(matches[1], rewriteUrl(matches[1], rebaseConfig, true)); + } +} + +function rebaseProperties(properties, validator, rebaseConfig) { + var property; + var value; + var i, l; + var j, m; + + for (i = 0, l = properties.length; i < l; i++) { + property = properties[i]; + + for (j = 2 /* 0 is Token.PROPERTY, 1 is name */, m = property.length; j < m; j++) { + value = property[j][1]; + + if (validator.isValidUrl(value)) { + property[j][1] = rewriteUrl(value, rebaseConfig); + } + } + } +} + +module.exports = rebase; diff --git a/lib/utils/restore-import.js b/lib/utils/restore-import.js new file mode 100644 index 00000000..5bdbd92c --- /dev/null +++ b/lib/utils/restore-import.js @@ -0,0 +1,5 @@ +function restoreImport(uri, mediaQuery) { + return ('@import ' + uri + ' ' + mediaQuery).trim(); +} + +module.exports = restoreImport; diff --git a/test/module-test.js b/test/module-test.js index b3537844..b019427a 100644 --- a/test/module-test.js +++ b/test/module-test.js @@ -447,14 +447,22 @@ vows.describe('module tests').addBatch({ assert.equal(minified.styles, '@import url(test/fixtures/partials/one.css);@import url(test/fixtures/partials/extra/three.css);@import url(test/fixtures/partials/extra/four.css);.two{color:#fff}'); } }, - 'off and rebase off': { + 'off - many files': { 'topic': function () { - return new CleanCSS({ processImport: false, rebase: false }).minify(['./test/fixtures/partials/two.css']); + return new CleanCSS({ processImport: false }).minify(['./test/fixtures/partials/remote.css', './test/fixtures/partials-absolute/base.css']); }, 'should give right output': function (minified) { - assert.equal(minified.styles, '@import url(test/fixtures/partials/one.css);@import url(test/fixtures/partials/extra/three.css);@import url(test/fixtures/partials/extra/four.css);.two{color:#fff}'); + assert.equal(minified.styles, '@import url(http://jakubpawlowicz.com/styles.css);@import url(test/fixtures/partials-absolute/extra/sub.css);.base{margin:0}'); } }, + 'off - many files with content': { + 'topic': function () { + return new CleanCSS({ processImport: false }).minify(['./test/fixtures/partials/two.css', './test/fixtures/partials-absolute/base.css']); + }, + 'should give right output': function (minified) { + assert.equal(minified.styles, '@import url(test/fixtures/partials/one.css);@import url(test/fixtures/partials/extra/three.css);@import url(test/fixtures/partials/extra/four.css);.two{color:#fff}.base{margin:0}'); + } + } } }, 'accepts a list of source files as hash': { diff --git a/test/optimizer/extract-properties-test.js b/test/optimizer/extract-properties-test.js index 209f9298..75fa125f 100644 --- a/test/optimizer/extract-properties-test.js +++ b/test/optimizer/extract-properties-test.js @@ -1,13 +1,20 @@ var vows = require('vows'); var assert = require('assert'); var tokenize = require('../../lib/tokenizer/tokenize'); +var inputSourceMapTracker = require('../../lib/utils/input-source-map-tracker'); var extractProperties = require('../../lib/optimizer/extract-properties'); +function _tokenize(source) { + return tokenize(source, { + inputSourceMapTracker: inputSourceMapTracker() + }); +} + vows.describe(extractProperties) .addBatch({ 'no properties': { 'topic': function () { - return extractProperties(tokenize('a{}', {})[0]); + return extractProperties(_tokenize('a{}')[0]); }, 'has no properties': function (tokens) { assert.deepEqual(tokens, []); @@ -15,7 +22,7 @@ vows.describe(extractProperties) }, 'no valid properties': { 'topic': function () { - return extractProperties(tokenize('a{:red}', {})[0]); + return extractProperties(_tokenize('a{:red}')[0]); }, 'has no properties': function (tokens) { assert.deepEqual(tokens, []); @@ -23,7 +30,7 @@ vows.describe(extractProperties) }, 'one property': { 'topic': function () { - return extractProperties(tokenize('a{color:red}', {})[0]); + return extractProperties(_tokenize('a{color:red}')[0]); }, 'has no properties': function (tokens) { assert.deepEqual(tokens, [ @@ -41,7 +48,7 @@ vows.describe(extractProperties) }, 'one important property': { 'topic': function () { - return extractProperties(tokenize('a{color:red!important}', {})[0]); + return extractProperties(_tokenize('a{color:red!important}')[0]); }, 'has no properties': function (tokens) { assert.deepEqual(tokens, [ @@ -59,7 +66,7 @@ vows.describe(extractProperties) }, 'one property - simple selector': { 'topic': function () { - return extractProperties(tokenize('#one span{color:red}', {})[0]); + return extractProperties(_tokenize('#one span{color:red}')[0]); }, 'has no properties': function (tokens) { assert.deepEqual(tokens, [ @@ -77,7 +84,7 @@ vows.describe(extractProperties) }, 'one property - variable': { 'topic': function () { - return extractProperties(tokenize('#one span{--color:red}', {})[0]); + return extractProperties(_tokenize('#one span{--color:red}')[0]); }, 'has no properties': function (tokens) { assert.deepEqual(tokens, []); @@ -85,7 +92,7 @@ vows.describe(extractProperties) }, 'one property - block variable': { 'topic': function () { - return extractProperties(tokenize('#one span{--color:{color:red;display:block};}', {})[0]); + return extractProperties(_tokenize('#one span{--color:{color:red;display:block};}')[0]); }, 'has no properties': function (tokens) { assert.deepEqual(tokens, []); @@ -93,7 +100,7 @@ vows.describe(extractProperties) }, 'one property - complex selector': { 'topic': function () { - return extractProperties(tokenize('.one{color:red}', {})[0]); + return extractProperties(_tokenize('.one{color:red}')[0]); }, 'has no properties': function (tokens) { assert.deepEqual(tokens, [ @@ -111,7 +118,7 @@ vows.describe(extractProperties) }, 'two properties': { 'topic': function () { - return extractProperties(tokenize('a{color:red;display:block}', {})[0]); + return extractProperties(_tokenize('a{color:red;display:block}')[0]); }, 'has no properties': function (tokens) { assert.deepEqual(tokens, [ @@ -138,7 +145,7 @@ vows.describe(extractProperties) }, 'from @media': { 'topic': function () { - return extractProperties(tokenize('@media{a{color:red;display:block}p{color:red}}', {})[0]); + return extractProperties(_tokenize('@media{a{color:red;display:block}p{color:red}}')[0]); }, 'has no properties': function (tokens) { assert.deepEqual(tokens, [ @@ -177,7 +184,7 @@ vows.describe(extractProperties) 'name root special cases': { 'vendor prefix': { 'topic': function () { - return extractProperties(tokenize('a{-moz-transform:none}', {})[0]); + return extractProperties(_tokenize('a{-moz-transform:none}')[0]); }, 'has no properties': function (tokens) { assert.deepEqual(tokens, [ @@ -195,7 +202,7 @@ vows.describe(extractProperties) }, 'list-style': { 'topic': function () { - return extractProperties(tokenize('a{list-style:none}', {})[0]); + return extractProperties(_tokenize('a{list-style:none}')[0]); }, 'has no properties': function (tokens) { assert.deepEqual(tokens, [ @@ -213,7 +220,7 @@ vows.describe(extractProperties) }, 'border-radius': { 'topic': function () { - return extractProperties(tokenize('a{border-top-left-radius:none}', {})[0]); + return extractProperties(_tokenize('a{border-top-left-radius:none}')[0]); }, 'has no properties': function (tokens) { assert.deepEqual(tokens, [ @@ -231,7 +238,7 @@ vows.describe(extractProperties) }, 'vendor prefixed border-radius': { 'topic': function () { - return extractProperties(tokenize('a{-webkit-border-top-left-radius:none}', {})[0]); + return extractProperties(_tokenize('a{-webkit-border-top-left-radius:none}')[0]); }, 'has no properties': function (tokens) { assert.deepEqual(tokens, [ @@ -249,7 +256,7 @@ vows.describe(extractProperties) }, 'border-image-width': { 'topic': function () { - return extractProperties(tokenize('a{border-image-width:2px}', {})[0]); + return extractProperties(_tokenize('a{border-image-width:2px}')[0]); }, 'has no properties': function (tokens) { assert.deepEqual(tokens, [ @@ -267,7 +274,7 @@ vows.describe(extractProperties) }, 'border-color': { 'topic': function () { - return extractProperties(tokenize('a{border-color:red}', {})[0]); + return extractProperties(_tokenize('a{border-color:red}')[0]); }, 'has no properties': function (tokens) { assert.deepEqual(tokens, [ @@ -285,7 +292,7 @@ vows.describe(extractProperties) }, 'border-top-style': { 'topic': function () { - return extractProperties(tokenize('a{border-top-style:none}', {})[0]); + return extractProperties(_tokenize('a{border-top-style:none}')[0]); }, 'has no properties': function (tokens) { assert.deepEqual(tokens, [ @@ -303,7 +310,7 @@ vows.describe(extractProperties) }, 'border-top': { 'topic': function () { - return extractProperties(tokenize('a{border-top:none}', {})[0]); + return extractProperties(_tokenize('a{border-top:none}')[0]); }, 'has no properties': function (tokens) { assert.deepEqual(tokens, [ @@ -321,7 +328,7 @@ vows.describe(extractProperties) }, 'border-collapse': { 'topic': function () { - return extractProperties(tokenize('a{border-collapse:collapse}', {})[0]); + return extractProperties(_tokenize('a{border-collapse:collapse}')[0]); }, 'has no properties': function (tokens) { assert.deepEqual(tokens, [ @@ -339,7 +346,7 @@ vows.describe(extractProperties) }, 'text-shadow': { 'topic': function () { - return extractProperties(tokenize('a{text-shadow:none}', {})[0]); + return extractProperties(_tokenize('a{text-shadow:none}')[0]); }, 'has no properties': function (tokens) { assert.deepEqual(tokens, [ diff --git a/test/optimizer/reorderable-test.js b/test/optimizer/reorderable-test.js index 5a291efa..ff5ad9bf 100644 --- a/test/optimizer/reorderable-test.js +++ b/test/optimizer/reorderable-test.js @@ -2,12 +2,21 @@ var vows = require('vows'); var assert = require('assert'); var tokenize = require('../../lib/tokenizer/tokenize'); +var inputSourceMapTracker = require('../../lib/utils/input-source-map-tracker'); var extractProperties = require('../../lib/optimizer/extract-properties'); var canReorder = require('../../lib/optimizer/reorderable').canReorder; var canReorderSingle = require('../../lib/optimizer/reorderable').canReorderSingle; function propertiesIn(source) { - return extractProperties(tokenize(source, { options: {} })[0]); + return extractProperties( + tokenize( + source, + { + inputSourceMapTracker: inputSourceMapTracker(), + options: {} + } + )[0] + ); } vows.describe(canReorder) diff --git a/test/properties/longhand-overriding-test.js b/test/properties/longhand-overriding-test.js index eb1f55c4..a13ada4d 100644 --- a/test/properties/longhand-overriding-test.js +++ b/test/properties/longhand-overriding-test.js @@ -4,11 +4,13 @@ var assert = require('assert'); var optimize = require('../../lib/properties/optimizer'); var tokenize = require('../../lib/tokenizer/tokenize'); +var inputSourceMapTracker = require('../../lib/utils/input-source-map-tracker'); var compatibility = require('../../lib/utils/compatibility'); var Validator = require('../../lib/properties/validator'); function _optimize(source) { var tokens = tokenize(source, { + inputSourceMapTracker: inputSourceMapTracker(), options: {}, warnings: [] }); diff --git a/test/properties/optimizer-test.js b/test/properties/optimizer-test.js index ed5d6933..05425a75 100644 --- a/test/properties/optimizer-test.js +++ b/test/properties/optimizer-test.js @@ -4,6 +4,7 @@ var assert = require('assert'); var optimize = require('../../lib/properties/optimizer'); var tokenize = require('../../lib/tokenizer/tokenize'); +var inputSourceMapTracker = require('../../lib/utils/input-source-map-tracker'); var compatibility = require('../../lib/utils/compatibility'); var Validator = require('../../lib/properties/validator'); @@ -12,6 +13,7 @@ function _optimize(source, mergeAdjacent, aggressiveMerging, compatibilityOption var validator = new Validator(compat); var tokens = tokenize(source, { + inputSourceMapTracker: inputSourceMapTracker(), options: {}, warnings: [] }); diff --git a/test/properties/override-compacting-test.js b/test/properties/override-compacting-test.js index d75d9d08..a43c7905 100644 --- a/test/properties/override-compacting-test.js +++ b/test/properties/override-compacting-test.js @@ -4,11 +4,13 @@ var assert = require('assert'); var optimize = require('../../lib/properties/optimizer'); var tokenize = require('../../lib/tokenizer/tokenize'); +var inputSourceMapTracker = require('../../lib/utils/input-source-map-tracker'); var compatibility = require('../../lib/utils/compatibility'); var Validator = require('../../lib/properties/validator'); function _optimize(source, compat, aggressiveMerging) { var tokens = tokenize(source, { + inputSourceMapTracker: inputSourceMapTracker(), options: {}, warnings: [] }); diff --git a/test/properties/shorthand-compacting-test.js b/test/properties/shorthand-compacting-test.js index 172e20a9..9980303c 100644 --- a/test/properties/shorthand-compacting-test.js +++ b/test/properties/shorthand-compacting-test.js @@ -4,11 +4,13 @@ var assert = require('assert'); var optimize = require('../../lib/properties/optimizer'); var tokenize = require('../../lib/tokenizer/tokenize'); +var inputSourceMapTracker = require('../../lib/utils/input-source-map-tracker'); var compatibility = require('../../lib/utils/compatibility'); var Validator = require('../../lib/properties/validator'); function _optimize(source) { var tokens = tokenize(source, { + inputSourceMapTracker: inputSourceMapTracker(), options: {}, warnings: [] }); @@ -35,7 +37,16 @@ vows.describe(optimize) assert.deepEqual(properties, [ [ 'property', - ['property-name', 'background'], + ['property-name', 'background', [ + [1, 2, undefined], + [1, 24, undefined], + [1, 56, undefined], + [1, 81, undefined], + [1, 105, undefined], + [1, 134, undefined], + [1, 155, undefined], + [1, 185, undefined] + ]], ['property-value', 'url(image.png)', [[1, 41, undefined]]], ['property-value', '#111', [[1, 19, undefined]]] ] @@ -50,7 +61,16 @@ vows.describe(optimize) assert.deepEqual(properties, [ [ 'property', - ['property-name', 'background'], + ['property-name', 'background', [ + [1, 2, undefined], + [1, 24, undefined], + [1, 56, undefined], + [1, 84, undefined], + [1, 108, undefined], + [1, 137, undefined], + [1, 158, undefined], + [1, 188, undefined] + ]], ['property-value', 'url(image.png)', [[1, 41, undefined]]], ['property-value', 'no-repeat', [[1, 74, undefined]]], ['property-value', '#111', [[1, 19, undefined]]] @@ -66,7 +86,16 @@ vows.describe(optimize) assert.deepEqual(properties, [ [ 'property', - ['property-name', 'background'], + ['property-name', 'background', [ + [1, 2, undefined], + [1, 34, undefined], + [1, 76, undefined], + [1, 111, undefined], + [1, 145, undefined], + [1, 184, undefined], + [1, 215, undefined], + [1, 255, undefined] + ]], ['property-value', 'url(image.png)', [[1, 51, undefined]]], ['property-value', '#111!important', [[1, 19, undefined]]] ] @@ -81,7 +110,12 @@ vows.describe(optimize) assert.deepEqual(properties, [ [ 'property', - ['property-name', 'border-width'], + ['property-name', 'border-width', [ + [1, 2, undefined], + [1, 23, undefined], + [1, 47, undefined], + [1, 69, undefined] + ]], ['property-value', '7px', [[1, 19, undefined]]], ['property-value', '4px', [[1, 88, undefined]]] ] @@ -96,7 +130,12 @@ vows.describe(optimize) assert.deepEqual(properties, [ [ 'property', - ['property-name', 'border-color'], + ['property-name', 'border-color', [ + [1, 2, undefined], + [1, 27, undefined], + [1, 55, undefined], + [1, 81, undefined] + ]], ['property-value', '#9fce00', [[1, 19, undefined]]] ] ]); @@ -110,7 +149,12 @@ vows.describe(optimize) assert.deepEqual(properties, [ [ 'property', - ['property-name', 'border-color'], + ['property-name', 'border-color', [ + [1, 2, undefined], + [1, 26, undefined], + [1, 51, undefined], + [1, 73, undefined] + ]], ['property-value', '#001', [[1, 68, undefined]]], ['property-value', '#002', [[1, 21, undefined]]], ['property-value', '#003', [[1, 46, undefined]]], @@ -127,7 +171,12 @@ vows.describe(optimize) assert.deepEqual(properties, [ [ 'property', - ['property-name', 'border-radius'], + ['property-name', 'border-radius', [ + [1, 2, undefined], + [1, 29, undefined], + [1, 60, undefined], + [1, 90, undefined] + ]], ['property-value', '7px', [[1, 25, undefined]]], ['property-value', '3px', [[1, 114, undefined]]], ['property-value', '6px', [[1, 56, undefined]]], @@ -160,7 +209,12 @@ vows.describe(optimize) assert.deepEqual(properties, [ [ 'property', - ['property-name', 'border-radius'], + ['property-name', 'border-radius', [ + [1, 2, undefined], + [1, 33, undefined], + [1, 65, undefined], + [1, 100, undefined] + ]], ['property-value', '7px', [[1, 25, undefined]]], ['property-value', '/'], ['property-value', '3px', [[1, 29, undefined]]] @@ -176,7 +230,12 @@ vows.describe(optimize) assert.deepEqual(properties, [ [ 'property', - ['property-name', 'border-radius'], + ['property-name', 'border-radius', [ + [1, 2, undefined], + [1, 33, undefined], + [1, 65, undefined], + [1, 100, undefined] + ]], ['property-value', '7px', [[1, 25, undefined]]], ['property-value', '6px', [[1, 57, undefined]]], ['property-value', '5px', [[1, 92, undefined]]], @@ -214,7 +273,11 @@ vows.describe(optimize) assert.deepEqual(properties, [ [ 'property', - ['property-name', 'list-style'], + ['property-name', 'list-style', [ + [1, 2, undefined], + [1, 25, undefined], + [1, 53, undefined] + ]], ['property-value', 'circle', [[1, 18, undefined]]], ['property-value', 'url(image.png)', [[1, 70, undefined]]] ] @@ -229,7 +292,11 @@ vows.describe(optimize) assert.deepEqual(properties, [ [ 'property', - ['property-name', 'list-style'], + ['property-name', 'list-style', [ + [1, 2, undefined], + [1, 34, undefined], + [1, 57, undefined] + ]], ['property-value', 'circle', [[1, 50, undefined]]], ['property-value', 'inside', [[1, 77, undefined]]], ['property-value', 'url(image.png)', [[1, 19, undefined]]] @@ -245,7 +312,12 @@ vows.describe(optimize) assert.deepEqual(properties, [ [ 'property', - ['property-name', 'margin'], + ['property-name', 'margin', [ + [1, 2, undefined], + [1, 18, undefined], + [1, 35, undefined], + [1, 53, undefined] + ]], ['property-value', '10px', [[1, 13, undefined]]], ['property-value', '5px', [[1, 31, undefined]]], ['property-value', '3px', [[1, 49, undefined]]], @@ -262,7 +334,12 @@ vows.describe(optimize) assert.deepEqual(properties, [ [ 'property', - ['property-name', 'padding'], + ['property-name', 'padding', [ + [1, 2, undefined], + [1, 19, undefined], + [1, 36, undefined], + [1, 55, undefined] + ]], ['property-value', '10px', [[1, 14, undefined]]], ['property-value', '2px', [[1, 69, undefined]]], ['property-value', '3px', [[1, 51, undefined]]], @@ -279,7 +356,12 @@ vows.describe(optimize) assert.deepEqual(properties, [ [ 'property', - ['property-name', 'padding'], + ['property-name', 'padding', [ + [1, 2, undefined], + [1, 34, undefined], + [1, 67, undefined], + [1, 104, undefined] + ]], ['property-value', '10px', [[1, 14, undefined]]], ['property-value', '2px', [[1, 118, undefined]]], ['property-value', '3px', [[1, 82, undefined]]], @@ -287,7 +369,12 @@ vows.describe(optimize) ], [ 'property', - ['property-name', 'margin'], + ['property-name', 'margin', [ + [1, 19, undefined], + [1, 51, undefined], + [1, 86, undefined], + [1, 122, undefined] + ]], ['property-value', '3px', [[1, 30, undefined]]] ] ]); @@ -311,7 +398,12 @@ vows.describe(optimize) ], [ 'property', - ['property-name', 'padding'], + ['property-name', 'padding', [ + [1, 2, undefined], + [1, 19, undefined], + [1, 36, undefined], + [1, 65, undefined] + ]], ['property-value', '10px', [[1, 14, undefined]]], ['property-value', '2px', [[1, 79, undefined]]], ['property-value', '3px', [[1, 51, undefined]]], diff --git a/test/protocol-imports-test.js b/test/protocol-imports-test.js index ad28bbf2..6f347dc3 100644 --- a/test/protocol-imports-test.js +++ b/test/protocol-imports-test.js @@ -168,7 +168,7 @@ vows.describe('protocol imports').addBatch({ }, 'should not raise errors': function (errors, minified) { assert.lengthOf(errors, 1); - assert.equal(errors[0], 'Broken @import declaration of "http://127.0.0.1/missing.css" - error 404'); + assert.equal(errors[0], 'Broken @import declaration of "http://127.0.0.1/missing.css" - 404'); }, 'should process @import': function (errors, minified) { assert.equal(minified.styles, '@import url(http://127.0.0.1/missing.css);p{font-size:13px}a{color:red}'); @@ -512,7 +512,7 @@ vows.describe('protocol imports').addBatch({ }, 'should raise warnings': function (error, minified) { assert.lengthOf(minified.warnings, 1); - assert.equal(minified.warnings[0], 'Skipping remote @import of "http://127.0.0.1/skipped.css" as resource not allowed.'); + assert.equal(minified.warnings[0], 'Skipping remote @import of "http://127.0.0.1/skipped.css" as resource is not allowed.'); }, 'should keep imports': function (error, minified) { assert.equal(minified.styles, '@import url(http://127.0.0.1/skipped.css);.one{color:red}'); @@ -795,8 +795,8 @@ vows.describe('protocol imports').addBatch({ }, 'should raise warnings': function (error, minified) { assert.lengthOf(minified.warnings, 2); - assert.equal(minified.warnings[0], 'Skipping remote @import of "http://127.0.0.1/remote.css" as resource not allowed.'); - assert.equal(minified.warnings[1], 'Skipping remote @import of "http://assets.127.0.0.1/remote.css" as resource not allowed.'); + assert.equal(minified.warnings[0], 'Skipping remote @import of "http://127.0.0.1/remote.css" as resource is not allowed.'); + assert.equal(minified.warnings[1], 'Skipping remote @import of "http://assets.127.0.0.1/remote.css" as resource is not allowed.'); }, 'should keeps imports': function (error, minified) { assert.equal(minified.styles, '@import url(http://127.0.0.1/remote.css);@import url(http://assets.127.0.0.1/remote.css);.one{color:red}'); @@ -864,9 +864,9 @@ vows.describe('protocol imports').addBatch({ }, 'should raise a warning': function (error, minified) { assert.lengthOf(minified.warnings, 3); - assert.equal(minified.warnings[0], 'Skipping remote @import of "http://127.0.0.1/remote.css" as resource not allowed.'); - assert.equal(minified.warnings[1], 'Skipping remote @import of "http://assets.127.0.0.1/remote.css" as resource not allowed.'); - assert.equal(minified.warnings[2], 'Skipping local @import of "test/fixtures/partials/one.css" as resource not allowed.'); + assert.equal(minified.warnings[0], 'Skipping remote @import of "http://127.0.0.1/remote.css" as resource is not allowed.'); + assert.equal(minified.warnings[1], 'Skipping remote @import of "http://assets.127.0.0.1/remote.css" as resource is not allowed.'); + assert.equal(minified.warnings[2], 'Skipping local @import of "test/fixtures/partials/one.css" as resource is not allowed.'); }, 'should process first imports': function (error, minified) { assert.equal(minified.styles, '@import url(http://127.0.0.1/remote.css);@import url(http://assets.127.0.0.1/remote.css);@import url(test/fixtures/partials/one.css);'); @@ -882,7 +882,7 @@ vows.describe('protocol imports').addBatch({ }, 'should raise a warning': function (error, minified) { assert.lengthOf(minified.warnings, 1); - assert.equal(minified.warnings[0], 'Skipping remote @import of "//127.0.0.1/remote.css" as resource not allowed.'); + assert.equal(minified.warnings[0], 'Skipping remote @import of "//127.0.0.1/remote.css" as resource is not allowed.'); }, 'should process first imports': function (error, minified) { assert.equal(minified.styles, '@import url(//127.0.0.1/remote.css);.one{color:red}'); diff --git a/test/source-map-test.js b/test/source-map-test.js index b1ba4a52..f6483676 100644 --- a/test/source-map-test.js +++ b/test/source-map-test.js @@ -1,20 +1,19 @@ /* jshint unused: false */ -var vows = require('vows'); var assert = require('assert'); -var CleanCSS = require('../index'); - var fs = require('fs'); +var http = require('http'); var path = require('path'); -var inputMapPath = path.join('test', 'fixtures', 'source-maps', 'styles.css.map'); -var inputMap = fs.readFileSync(inputMapPath, 'utf-8'); -var nock = require('nock'); -var http = require('http'); var enableDestroy = require('server-destroy'); - +var nock = require('nock'); +var vows = require('vows'); var port = 24682; +var CleanCSS = require('../index'); + +var inputMapPath = path.join('test', 'fixtures', 'source-maps', 'styles.css.map'); +var inputMap = fs.readFileSync(inputMapPath, 'utf-8'); var lineBreak = require('os').EOL; var escape = global.escape; @@ -91,7 +90,18 @@ vows.describe('source-map') return new CleanCSS({ sourceMap: true }).minify('/*! a */div[data-id=" abc "] { color:red; }'); }, 'has 3 mappings': function (minified) { - assert.lengthOf(minified.sourceMap._mappings._array, 3); + assert.lengthOf(minified.sourceMap._mappings._array, 4); + }, + 'has comment mapping': function (minified) { + var mapping = { + generatedLine: 1, + generatedColumn: 0, + originalLine: 1, + originalColumn: 0, + source: '$stdin', + name: null + }; + assert.deepEqual(minified.sourceMap._mappings._array[0], mapping); }, 'has selector mapping': function (minified) { var mapping = { @@ -102,7 +112,7 @@ vows.describe('source-map') source: '$stdin', name: null }; - assert.deepEqual(minified.sourceMap._mappings._array[0], mapping); + assert.deepEqual(minified.sourceMap._mappings._array[1], mapping); }, 'has name mapping': function (minified) { var mapping = { @@ -113,7 +123,7 @@ vows.describe('source-map') source: '$stdin', name: null }; - assert.deepEqual(minified.sourceMap._mappings._array[1], mapping); + assert.deepEqual(minified.sourceMap._mappings._array[2], mapping); }, 'has value mapping': function (minified) { var mapping = { @@ -124,7 +134,7 @@ vows.describe('source-map') source: '$stdin', name: null }; - assert.deepEqual(minified.sourceMap._mappings._array[2], mapping); + assert.deepEqual(minified.sourceMap._mappings._array[3], mapping); } }, 'module #2': { @@ -479,7 +489,7 @@ vows.describe('source-map') generatedColumn: 0, originalLine: 1, originalColumn: 4, - source: 'test/fixtures/source-maps/styles.less', + source: path.join('test', 'fixtures', 'source-maps', 'styles.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[0], mapping); @@ -490,7 +500,7 @@ vows.describe('source-map') generatedColumn: 6, originalLine: 2, originalColumn: 2, - source: 'test/fixtures/source-maps/styles.less', + source: path.join('test', 'fixtures', 'source-maps', 'styles.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[1], mapping); @@ -501,15 +511,15 @@ vows.describe('source-map') generatedColumn: 12, originalLine: 2, originalColumn: 2, - source: 'test/fixtures/source-maps/styles.less', + source: path.join('test', 'fixtures', 'source-maps', 'styles.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[2], mapping); } }, - 'input map from source with root': { + 'input map from source with rebaseTo': { 'topic': function () { - return new CleanCSS({ sourceMap: true, root: './test/fixtures' }).minify('div > a {\n color: red;\n}/*# sourceMappingURL=source-maps/styles.css.map */'); + return new CleanCSS({ sourceMap: true, rebaseTo: './test/fixtures' }).minify('div > a {\n color: red;\n}/*# sourceMappingURL=' + inputMapPath + ' */'); }, 'has 3 mappings': function (minified) { assert.lengthOf(minified.sourceMap._mappings._array, 3); @@ -520,7 +530,7 @@ vows.describe('source-map') generatedColumn: 0, originalLine: 1, originalColumn: 4, - source: 'source-maps/styles.less', + source: path.join('source-maps', 'styles.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[0], mapping); @@ -531,7 +541,7 @@ vows.describe('source-map') generatedColumn: 6, originalLine: 2, originalColumn: 2, - source: 'source-maps/styles.less', + source: path.join('source-maps', 'styles.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[1], mapping); @@ -542,48 +552,7 @@ vows.describe('source-map') generatedColumn: 12, originalLine: 2, originalColumn: 2, - source: 'source-maps/styles.less', - name: null - }; - assert.deepEqual(minified.sourceMap._mappings._array[2], mapping); - } - }, - 'input map from source with target': { - 'topic': function () { - return new CleanCSS({ sourceMap: true, target: './test' }).minify('div > a {\n color: red;\n}/*# sourceMappingURL=' + inputMapPath + ' */'); - }, - 'has 3 mappings': function (minified) { - assert.lengthOf(minified.sourceMap._mappings._array, 3); - }, - 'has `div > a` mapping': function (minified) { - var mapping = { - generatedLine: 1, - generatedColumn: 0, - originalLine: 1, - originalColumn: 4, - source: 'fixtures/source-maps/styles.less', - name: null - }; - assert.deepEqual(minified.sourceMap._mappings._array[0], mapping); - }, - 'has `color` mapping': function (minified) { - var mapping = { - generatedLine: 1, - generatedColumn: 6, - originalLine: 2, - originalColumn: 2, - source: 'fixtures/source-maps/styles.less', - name: null - }; - assert.deepEqual(minified.sourceMap._mappings._array[1], mapping); - }, - 'has `red` mapping': function (minified) { - var mapping = { - generatedLine: 1, - generatedColumn: 12, - originalLine: 2, - originalColumn: 2, - source: 'fixtures/source-maps/styles.less', + source: path.join('source-maps', 'styles.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[2], mapping); @@ -598,7 +567,7 @@ vows.describe('source-map') 'input map as inlined data URI with explicit charset us-ascii, not base64': inlineDataUriContext('data:application/json;charset=us-ascii,' + escape(inputMap)), 'complex input map': { 'topic': function () { - return new CleanCSS({ sourceMap: true, root: path.dirname(inputMapPath) }).minify('@import url(import.css);'); + return new CleanCSS({ sourceMap: true }).minify('@import url(' + path.dirname(inputMapPath) + '/import.css);'); }, 'has 6 mappings': function (minified) { assert.lengthOf(minified.sourceMap._mappings._array, 6); @@ -609,7 +578,7 @@ vows.describe('source-map') generatedColumn: 0, originalLine: 1, originalColumn: 0, - source: 'some.less', + source: path.join(path.dirname(inputMapPath), 'some.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[0], mapping); @@ -620,7 +589,7 @@ vows.describe('source-map') generatedColumn: 4, originalLine: 2, originalColumn: 2, - source: 'some.less', + source: path.join(path.dirname(inputMapPath), 'some.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[1], mapping); @@ -631,7 +600,7 @@ vows.describe('source-map') generatedColumn: 10, originalLine: 2, originalColumn: 2, - source: 'some.less', + source: path.join(path.dirname(inputMapPath), 'some.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[2], mapping); @@ -642,7 +611,7 @@ vows.describe('source-map') generatedColumn: 14, originalLine: 1, originalColumn: 4, - source: 'styles.less', + source: path.join(path.dirname(inputMapPath), 'styles.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[3], mapping); @@ -653,7 +622,7 @@ vows.describe('source-map') generatedColumn: 20, originalLine: 2, originalColumn: 2, - source: 'styles.less', + source: path.join(path.dirname(inputMapPath), 'styles.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[4], mapping); @@ -664,20 +633,12 @@ vows.describe('source-map') generatedColumn: 26, originalLine: 2, originalColumn: 2, - source: 'styles.less', + source: path.join(path.dirname(inputMapPath), 'styles.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[5], mapping); } }, - 'complex input map referenced by path': { - 'topic': function () { - return new CleanCSS({ sourceMap: true }).minify('@import url(test/fixtures/source-maps/import.css);'); - }, - 'has 6 mappings': function (minified) { - assert.lengthOf(minified.sourceMap._mappings._array, 6); - } - }, 'complex but partial input map referenced by path': { 'topic': function () { return new CleanCSS({ sourceMap: true }).minify('@import url(test/fixtures/source-maps/no-map-import.css);'); @@ -687,20 +648,20 @@ vows.describe('source-map') }, 'has 3 mappings to .less file': function (minified) { var fromLess = minified.sourceMap._mappings._array.filter(function (mapping) { - return mapping.source == 'test/fixtures/source-maps/styles.less'; + return mapping.source == path.join('test', 'fixtures', 'source-maps', 'styles.less'); }); assert.lengthOf(fromLess, 3); }, 'has 3 mappings to .css file': function (minified) { var fromCSS = minified.sourceMap._mappings._array.filter(function (mapping) { - return mapping.source == 'test/fixtures/source-maps/no-map.css'; + return mapping.source == path.join('test', 'fixtures', 'source-maps', 'no-map.css'); }); assert.lengthOf(fromCSS, 3); } }, - 'complex input map with an existing file as target': { + 'complex input map with an existing file as rebaseTo': { 'topic': function () { - return new CleanCSS({ sourceMap: true, target: path.join('test', 'fixtures', 'source-maps', 'styles.css') }).minify('@import url(test/fixtures/source-maps/styles.css);'); + return new CleanCSS({ sourceMap: true, rebaseTo: path.join('test', 'fixtures', 'source-maps') }).minify('@import url(test/fixtures/source-maps/styles.css);'); }, 'has 3 mappings': function (minified) { assert.lengthOf(minified.sourceMap._mappings._array, 3); @@ -725,7 +686,7 @@ vows.describe('source-map') generatedColumn: 0, originalLine: 2, originalColumn: 8, - source: 'test/fixtures/source-maps/nested/once.less', + source: path.join('test', 'fixtures', 'source-maps', 'nested/once.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[0], mapping); @@ -736,7 +697,7 @@ vows.describe('source-map') generatedColumn: 14, originalLine: 3, originalColumn: 4, - source: 'test/fixtures/source-maps/nested/once.less', + source: path.join('test', 'fixtures', 'source-maps', 'nested/once.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[1], mapping); @@ -747,7 +708,7 @@ vows.describe('source-map') generatedColumn: 20, originalLine: 3, originalColumn: 4, - source: 'test/fixtures/source-maps/nested/once.less', + source: path.join('test', 'fixtures', 'source-maps', 'nested/once.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[2], mapping); @@ -766,7 +727,7 @@ vows.describe('source-map') generatedColumn: 0, originalLine: 3, originalColumn: 4, - source: 'test/fixtures/source-maps/nested/twice.less', + source: path.join('test', 'fixtures', 'source-maps', 'nested/twice.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[0], mapping); @@ -777,7 +738,7 @@ vows.describe('source-map') generatedColumn: 11, originalLine: 4, originalColumn: 6, - source: 'test/fixtures/source-maps/nested/twice.less', + source: path.join('test', 'fixtures', 'source-maps', 'nested/twice.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[1], mapping); @@ -788,7 +749,7 @@ vows.describe('source-map') generatedColumn: 17, originalLine: 4, originalColumn: 6, - source: 'test/fixtures/source-maps/nested/twice.less', + source: path.join('test', 'fixtures', 'source-maps', 'nested/twice.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[2], mapping); @@ -935,9 +896,12 @@ vows.describe('source-map') 'has mapping': function (errors, minified) { assert.isDefined(minified.sourceMap); }, - 'raises an error': function (errors, _) { - assert.lengthOf(errors, 1); - assert.equal(errors[0], 'Broken source map at "http://127.0.0.1/remote.css.map" - 404'); + 'raises no errors': function (errors, _) { + assert.isNull(errors); + }, + 'raises a warning': function (_, minified) { + assert.lengthOf(minified.warnings, 1); + assert.equal(minified.warnings[0], 'Missing source map at "http://127.0.0.1/remote.css.map" - 404'); }, teardown: function () { assert.isTrue(this.reqMocks.isDone()); @@ -971,9 +935,12 @@ vows.describe('source-map') 'has mapping': function (errors, minified) { assert.isDefined(minified.sourceMap); }, - 'raises an error': function (errors, _) { - assert.lengthOf(errors, 1); - assert.include(errors[0], 'Broken source map at "http://127.0.0.1:' + port + '/remote.css.map"'); + 'raises no errors': function (errors, _) { + assert.isNull(errors); + }, + 'raises a warning': function (_, minified) { + assert.lengthOf(minified.warnings, 1); + assert.equal(minified.warnings[0], 'Missing source map at "http://127.0.0.1:24682/remote.css.map" - timeout'); }, teardown: function () { this.server.destroy(); @@ -1005,7 +972,7 @@ vows.describe('source-map') topic: function () { this.reqMocks = nock('https://127.0.0.1') .get('/remote.css') - .reply(200, '/*# sourceMappingURL=https://127.0.0.1/remote.css.map */') + .reply(200, 'div>a{color:blue}/*# sourceMappingURL=https://127.0.0.1/remote.css.map */') .get('/remote.css.map') .reply(200, inputMap); @@ -1014,6 +981,9 @@ vows.describe('source-map') 'has mapping': function (errors, minified) { assert.isDefined(minified.sourceMap); }, + 'maps to external source file': function (errors, minified) { + assert.equal(minified.sourceMap._mappings._array[0].source, 'https://127.0.0.1/styles.less'); + }, teardown: function () { assert.isTrue(this.reqMocks.isDone()); nock.cleanAll(); @@ -1023,7 +993,7 @@ vows.describe('source-map') topic: function () { this.reqMocks = nock('http://127.0.0.1') .get('/remote.css') - .reply(200, '/*# sourceMappingURL=remote.css.map */') + .reply(200, 'div>a{color:blue}/*# sourceMappingURL=remote.css.map */') .get('/remote.css.map') .reply(200, inputMap); @@ -1032,6 +1002,9 @@ vows.describe('source-map') 'has mapping': function (errors, minified) { assert.isDefined(minified.sourceMap); }, + 'maps to external source file': function (errors, minified) { + assert.equal(minified.sourceMap._mappings._array[0].source, 'http://127.0.0.1/styles.less'); + }, teardown: function () { assert.isTrue(this.reqMocks.isDone()); nock.cleanAll(); @@ -1041,7 +1014,7 @@ vows.describe('source-map') topic: function () { this.reqMocks = nock('http://127.0.0.1') .post('/remote.css') - .reply(200, '/*# sourceMappingURL=remote.css.map */') + .reply(200, 'div>a{color:blue}/*# sourceMappingURL=remote.css.map */') .post('/remote.css.map') .reply(200, inputMap); @@ -1051,6 +1024,9 @@ vows.describe('source-map') 'has mapping': function (errors, minified) { assert.isDefined(minified.sourceMap); }, + 'maps to external source file': function (errors, minified) { + assert.equal(minified.sourceMap._mappings._array[0].source, 'http://127.0.0.1/styles.less'); + }, teardown: function () { assert.isTrue(this.reqMocks.isDone()); nock.cleanAll(); @@ -1087,7 +1063,7 @@ vows.describe('source-map') return new CleanCSS({ sourceMap: true, keepSpecialComments: 1 }).minify('div { color: #f00 !important; /*!1*/} /*!2*/ a{/*!3*/}'); }, 'has right output': function (errors, minified) { - assert.equal(minified.styles, 'div{color:red!important/*!1*/}a{}'); + assert.equal(minified.styles, 'div{color:red!important/*!1*/}'); } } }) @@ -1122,7 +1098,7 @@ vows.describe('source-map') generatedColumn: 0, originalLine: 1, originalColumn: 0, - source: 'test/fixtures/source-maps/some.less', + source: path.join('test', 'fixtures', 'source-maps', 'some.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[0], mapping); @@ -1133,7 +1109,7 @@ vows.describe('source-map') generatedColumn: 4, originalLine: 2, originalColumn: 8, - source: 'test/fixtures/source-maps/nested/once.less', + source: path.join('test', 'fixtures', 'source-maps', 'nested/once.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[1], mapping); @@ -1144,7 +1120,7 @@ vows.describe('source-map') generatedColumn: 18, originalLine: 2, originalColumn: 2, - source: 'test/fixtures/source-maps/some.less', + source: path.join('test', 'fixtures', 'source-maps', 'some.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[2], mapping); @@ -1155,7 +1131,7 @@ vows.describe('source-map') generatedColumn: 24, originalLine: 2, originalColumn: 2, - source: 'test/fixtures/source-maps/some.less', + source: path.join('test', 'fixtures', 'source-maps', 'some.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[3], mapping); @@ -1166,7 +1142,7 @@ vows.describe('source-map') generatedColumn: 28, originalLine: 1, originalColumn: 4, - source: 'test/fixtures/source-maps/styles.less', + source: path.join('test', 'fixtures', 'source-maps', 'styles.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[4], mapping); @@ -1177,7 +1153,7 @@ vows.describe('source-map') generatedColumn: 34, originalLine: 2, originalColumn: 2, - source: 'test/fixtures/source-maps/styles.less', + source: path.join('test', 'fixtures', 'source-maps', 'styles.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[5], mapping); @@ -1188,7 +1164,7 @@ vows.describe('source-map') generatedColumn: 40, originalLine: 2, originalColumn: 2, - source: 'test/fixtures/source-maps/styles.less', + source: path.join('test', 'fixtures', 'source-maps', 'styles.less'), name: null }; assert.deepEqual(minified.sourceMap._mappings._array[6], mapping); @@ -1198,7 +1174,7 @@ vows.describe('source-map') 'relative to path': { 'complex but partial input map referenced by path': { 'topic': function () { - return new CleanCSS({ sourceMap: true, target: './test' }).minify({ + return new CleanCSS({ sourceMap: true, rebaseTo: './test' }).minify({ 'test/fixtures/source-maps/some.css': { styles: 'div {\n color: red;\n}', sourceMap: '{"version":3,"sources":["some.less"],"names":[],"mappings":"AAAA;EACE,UAAA","file":"some.css"}' @@ -1224,9 +1200,9 @@ vows.describe('source-map') }); assert.deepEqual(sources, [ - 'fixtures/source-maps/some.less', - 'fixtures/source-maps/nested/once.less', - 'fixtures/source-maps/styles.less' + path.join('fixtures', 'source-maps', 'some.less'), + path.join('fixtures', 'source-maps', 'nested', 'once.less'), + path.join('fixtures', 'source-maps', 'styles.less') ]); } } @@ -1298,8 +1274,8 @@ vows.describe('source-map') 'from array - off': { 'topic': function () { return new CleanCSS({ sourceMap: true }).minify([ - 'test/fixtures/partials/one.css', - 'test/fixtures/partials/three.css' + path.join('test', 'fixtures', 'partials', 'one.css'), + path.join('test', 'fixtures', 'partials', 'three.css') ]); }, 'has 6 mappings': function (minified) { @@ -1307,8 +1283,8 @@ vows.describe('source-map') }, 'has embedded sources': function (minified) { assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [ - 'test/fixtures/partials/one.css', - 'test/fixtures/partials/three.css' + path.join('test', 'fixtures', 'partials', 'one.css'), + path.join('test', 'fixtures', 'partials', 'three.css') ]); }, 'has embedded sources content': function (minified) { @@ -1318,8 +1294,8 @@ vows.describe('source-map') 'from array - on': { 'topic': function () { return new CleanCSS({ sourceMap: true, sourceMapInlineSources: true }).minify([ - 'test/fixtures/partials/one.css', - 'test/fixtures/partials/three.css' + path.join('test', 'fixtures', 'partials', 'one.css'), + path.join('test', 'fixtures', 'partials', 'three.css') ]); }, 'has 6 mappings': function (minified) { @@ -1327,14 +1303,14 @@ vows.describe('source-map') }, 'has embedded sources': function (minified) { assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [ - 'test/fixtures/partials/one.css', - 'test/fixtures/partials/three.css' + path.join('test', 'fixtures', 'partials', 'one.css'), + path.join('test', 'fixtures', 'partials', 'three.css') ]); }, 'has embedded sources content': function (minified) { assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [ - '.one { color:#f00; }' + lineBreak, - '.three {background-image: url(test/fixtures/partials/extra/down.gif);}' + lineBreak + '.one { color:#f00; }\n', + '.three {background-image: url(extra/down.gif);}\n' ]); } }, @@ -1358,7 +1334,7 @@ vows.describe('source-map') }, 'has embedded sources content': function (minified) { assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [ - 'div{background:url(http://127.0.0.1/image.png)}', + 'div{background:url(image.png)}', ]); }, 'teardown': function () { @@ -1502,9 +1478,9 @@ vows.describe('source-map') }, 'has embedded sources': function (minified) { assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [ - 'test/fixtures/source-maps/some.less', - 'test/fixtures/source-maps/nested/once.less', - 'test/fixtures/source-maps/styles.less' + path.join('test', 'fixtures', 'source-maps', 'some.less'), + path.join('test', 'fixtures', 'source-maps', 'nested', 'once.less'), + path.join('test', 'fixtures', 'source-maps', 'styles.less') ]); }, 'has embedded sources content': function (minified) { @@ -1515,9 +1491,9 @@ vows.describe('source-map') ]); } }, - 'multiple relative to a target path': { + 'multiple relative to rebaseTo path': { 'topic': function () { - return new CleanCSS({ sourceMap: true, sourceMapInlineSources: true, target: path.join(process.cwd(), 'test') }).minify({ + return new CleanCSS({ sourceMap: true, sourceMapInlineSources: true, rebaseTo: './test' }).minify({ 'test/fixtures/source-maps/some.css': { styles: 'div {\n color: red;\n}', sourceMap: '{"version":3,"sources":["some.less"],"names":[],"mappings":"AAAA;EACE,UAAA","file":"some.css","sourcesContent":["div {\\n color: red;\\n}\\n"]}' @@ -1537,9 +1513,9 @@ vows.describe('source-map') }, 'has embedded sources': function (minified) { assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [ - 'fixtures/source-maps/some.less', - 'fixtures/source-maps/nested/once.less', - 'fixtures/source-maps/styles.less' + path.join('fixtures', 'source-maps', 'some.less'), + path.join('fixtures', 'source-maps', 'nested', 'once.less'), + path.join('fixtures', 'source-maps', 'styles.less') ]); }, 'has embedded sources content': function (minified) { @@ -1572,16 +1548,16 @@ vows.describe('source-map') }, 'has embedded sources': function (minified) { assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [ - 'test/fixtures/source-maps/some.less', - 'test/fixtures/source-maps/nested/once.less', - 'test/fixtures/source-maps/styles.less' + path.join('test', 'fixtures', 'source-maps', 'some.less'), + path.join('test', 'fixtures', 'source-maps', 'nested', 'once.less'), + path.join('test', 'fixtures', 'source-maps', 'styles.less') ]); }, 'has embedded sources content': function (minified) { assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [ 'div {\n color: red;\n}\n', 'section {\n > div a {\n color:red;\n }\n}\n', - 'div > a {' + lineBreak + ' color: blue;' + lineBreak + '}' + lineBreak + 'div > a {\n color: blue;\n}\n' ]); } }, @@ -1607,9 +1583,9 @@ vows.describe('source-map') }, 'has embedded sources': function (minified) { assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [ - 'test/fixtures/source-maps/some.less', - 'test/fixtures/source-maps/nested/once.less', - 'test/fixtures/source-maps/styles.less' + path.join('test', 'fixtures', 'source-maps', 'some.less'), + path.join('test', 'fixtures', 'source-maps', 'nested', 'once.less'), + path.join('test', 'fixtures', 'source-maps', 'styles.less') ]); }, 'has embedded sources content': function (minified) { @@ -1639,20 +1615,20 @@ vows.describe('source-map') } }, this.callback); }, - 'has 7 mappings': function (minified) { + 'has 7 mappings': function (errors, minified) { assert.lengthOf(minified.sourceMap._mappings._array, 7); }, - 'has embedded sources': function (minified) { + 'has embedded sources': function (errors, minified) { assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [ 'http://127.0.0.1/some.less', - 'test/fixtures/source-maps/nested/once.less', + path.join('test', 'fixtures', 'source-maps', 'nested', 'once.less'), 'http://127.0.0.1/styles.less' ]); }, - 'has embedded sources content': function (minified) { + 'has embedded sources content': function (errors, minified) { assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [ 'div {\n color: red;\n}\n', - 'section {' + lineBreak + ' > div a {' + lineBreak + ' color:red;' + lineBreak + ' }' + lineBreak + '}' + lineBreak, + 'section {\n > div a {\n color:red;\n }\n}\n', 'div > a {\n color: blue;\n}\n' ]); }, @@ -1688,19 +1664,19 @@ vows.describe('source-map') assert.lengthOf(minified.sourceMap._mappings._array, 7); }, 'should warn about some.less': function (minified) { - assert.deepEqual(minified.warnings, ['Broken original source file at "http://127.0.0.1/some.less" - 404']); + assert.deepEqual(minified.warnings, ['Missing original source at "http://127.0.0.1/some.less" - 404']); }, 'has embedded sources': function (minified) { assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [ 'http://127.0.0.1/some.less', - 'test/fixtures/source-maps/nested/once.less', + path.join('test', 'fixtures', 'source-maps', 'nested', 'once.less'), 'http://127.0.0.1/styles.less' ]); }, 'has embedded sources content': function (minified) { assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [ null, - 'section {' + lineBreak + ' > div a {' + lineBreak + ' color:red;' + lineBreak + ' }' + lineBreak + '}' + lineBreak, + 'section {\n > div a {\n color:red;\n }\n}\n', 'div > a {\n color: blue;\n}\n' ]); }, @@ -1731,21 +1707,21 @@ vows.describe('source-map') }, 'should warn about some.less and styles.less': function (minified) { assert.deepEqual(minified.warnings, [ - 'No callback given to `#minify` method, cannot fetch a remote file from "http://127.0.0.1/some.less"', - 'No callback given to `#minify` method, cannot fetch a remote file from "http://127.0.0.1/styles.less"' + 'Cannot fetch remote resource from "http://127.0.0.1/some.less" as no callback given.', + 'Cannot fetch remote resource from "http://127.0.0.1/styles.less" as no callback given.' ]); }, 'has embedded sources': function (minified) { assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [ 'http://127.0.0.1/some.less', - 'test/fixtures/source-maps/nested/once.less', + path.join('test', 'fixtures', 'source-maps', 'nested', 'once.less'), 'http://127.0.0.1/styles.less' ]); }, 'has embedded sources content': function (minified) { assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [ null, - 'section {' + lineBreak + ' > div a {' + lineBreak + ' color:red;' + lineBreak + ' }' + lineBreak + '}' + lineBreak, + 'section {\n > div a {\n color:red;\n }\n}\n', null ]); } diff --git a/test/tokenizer/tokenize-test.js b/test/tokenizer/tokenize-test.js index 14c175b7..379a4645 100644 --- a/test/tokenizer/tokenize-test.js +++ b/test/tokenizer/tokenize-test.js @@ -1,7 +1,7 @@ var vows = require('vows'); var assert = require('assert'); var tokenize = require('../../lib/tokenizer/tokenize'); -var inputSourceMapTracker = require('../../lib/utils/input-source-map-tracker-2'); +var inputSourceMapTracker = require('../../lib/utils/input-source-map-tracker'); var fs = require('fs'); var path = require('path'); @@ -20,6 +20,7 @@ function tokenizerContext(group, specs) { function toTokens(source) { return function () { return tokenize(source, { + inputSourceMapTracker: inputSourceMapTracker(), options: {}, warnings: [] }); @@ -3426,8 +3427,9 @@ vows.describe(tokenize) var warnings = []; tokenize('a{display:block', { - source: 'one.css', + inputSourceMapTracker: inputSourceMapTracker(), options: {}, + source: 'one.css', warnings: warnings }); @@ -3442,8 +3444,9 @@ vows.describe(tokenize) 'sources - rule with properties': { 'topic': function () { return tokenize('a{color:red}', { - source: 'one.css', + inputSourceMapTracker: inputSourceMapTracker(), options: {}, + source: 'one.css', warnings: [] }); }, @@ -3487,14 +3490,11 @@ vows.describe(tokenize) .addBatch({ 'input source maps - simple': { 'topic': function () { - var sourceMapTracker = inputSourceMapTracker({ - errors: {} - }); + var sourceMapTracker = inputSourceMapTracker(); sourceMapTracker.track('styles.css', inputMap); return tokenize('div > a {\n color: red;\n}', { source: 'styles.css', - inputSourceMap: true, inputSourceMapTracker: sourceMapTracker, options: {}, warnings: [] @@ -3509,7 +3509,7 @@ vows.describe(tokenize) 'rule-scope', 'div > a', [ - [1, 0, 'styles.less'] + [1, 4, 'styles.less'] ] ] ], @@ -3538,14 +3538,11 @@ vows.describe(tokenize) }, 'with fallback for properties': { 'topic': function () { - var sourceMapTracker = inputSourceMapTracker({ - errors: {} - }); + var sourceMapTracker = inputSourceMapTracker(); sourceMapTracker.track('styles.css', inputMap); return tokenize('div > a {\n color: red red;\n}', { source: 'styles.css', - inputSourceMap: true, inputSourceMapTracker: sourceMapTracker, options: {}, warnings: [] @@ -3560,7 +3557,7 @@ vows.describe(tokenize) 'rule-scope', 'div > a', [ - [1, 0, 'styles.less'] + [1, 4, 'styles.less'] ] ] ],