Reworks inlining so resources are loaded lazily.
authorJakub Pawlowicz <contact@jakubpawlowicz.com>
Mon, 26 Dec 2016 11:42:08 +0000 (12:42 +0100)
committerJakub Pawlowicz <contact@jakubpawlowicz.com>
Mon, 26 Dec 2016 12:01:14 +0000 (13:01 +0100)
Why:

* when using a hash as an input, the sources it references
  should be processed lazily instead of a whole hash being
  serialized & tokenized eagerly.

This change addresses the need by:

* See #791 - in-memory imports require such lazy processing.
  The other way, we end up with duplicated rules, and have to rely
  on advanced processing to remove them.

lib/reader/is-already-loaded.js [deleted file]
lib/reader/read-sources.js
test/module-test.js

diff --git a/lib/reader/is-already-loaded.js b/lib/reader/is-already-loaded.js
deleted file mode 100644 (file)
index 7a37dc3..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-var restoreImport = require('./restore-import');
-
-var Marker = require('../tokenizer/marker');
-
-function isAlreadyLoaded(uri, sourcesContent) {
-  // the second check is because clean-css accepts an array of input file paths
-  // which are internally transformed into imports
-  return uri in sourcesContent && sourcesContent[uri] != (restoreImport(uri, '') + Marker.SEMICOLON);
-}
-
-module.exports = isAlreadyLoaded;
index d00ecba..8715cfb 100644 (file)
@@ -4,7 +4,6 @@ var path = require('path');
 var applySourceMaps = require('./apply-source-maps');
 var extractImportUrlAndMedia = require('./extract-import-url-and-media');
 var isAllowedResource = require('./is-allowed-resource');
-var isAlreadyLoaded = require('./is-already-loaded');
 var loadOriginalSources = require('./load-original-sources');
 var loadRemoteResource = require('./load-remote-resource');
 var rebase = require('./rebase');
@@ -33,111 +32,106 @@ function readSources(input, context, callback) {
 
 function doReadSources(input, context, callback) {
   if (typeof input == 'string') {
-    return fromString(input, context, {}, callback);
+    return fromString(input, context, callback);
   } else if (Buffer.isBuffer(input)) {
-    return fromString(input.toString(), context, {}, callback);
+    return fromString(input.toString(), context, callback);
   } else if (Array.isArray(input)) {
-    return fromArray(input, context, {}, callback);
+    return fromArray(input, context, callback);
   } else if (typeof input == 'object') {
-    return fromHash(input, context, {}, callback);
+    return fromHash(input, context, callback);
   }
 }
 
-function fromString(input, context, parentInlinerContext, callback) {
-  var inputAsHash = {};
+function fromString(input, context, callback) {
+  context.source = undefined;
+  context.sourcesContent[undefined] = input;
+  context.stats.originalSize += input.length;
 
-  inputAsHash[UNKNOWN_URI] = {
-    styles: input,
-    sourceMap: (typeof context.options.sourceMap !== 'boolean') ? context.options.sourceMap : null
-  };
+  if (context.options.sourceMap && (typeof context.options.sourceMap !== 'boolean')) {
+    trackSourceMap(context.options.sourceMap, undefined, context);
+  }
 
-  return fromHash(inputAsHash, context, parentInlinerContext, callback);
+  return fromStyles(input, context, { processImports: context.options.processImport }, callback);
 }
 
-function fromArray(input, context, parentInlinerContext, callback) {
-  var currentPath = path.resolve('');
-  var inputAsHash = input.reduce(function (accumulator, uri) {
-    var absoluteUri = isAbsoluteResource(uri) || isRemoteResource(uri) ?
-      uri :
-      path.resolve(uri);
-    var relativeToCurrentPath;
-
-    if (isRemoteResource(uri)) {
-      accumulator[uri] = {
-        styles: restoreImport(uri, '') + Marker.SEMICOLON
-      };
-    } else if (!fs.existsSync(absoluteUri) || !fs.statSync(absoluteUri).isFile()) {
-      context.errors.push('Ignoring "' + absoluteUri + '" as resource is missing.');
-    } else {
-      relativeToCurrentPath = path.relative(currentPath, absoluteUri);
-      accumulator[relativeToCurrentPath] = {
-        styles: fs.readFileSync(absoluteUri, 'utf-8')
-      };
-    }
-
+function fromArray(input, context, callback) {
+  var inputAsImports = input.reduce(function (accumulator, uri) {
+    accumulator.push(restoreAsRelativeImport(uri));
     return accumulator;
-  }, {});
+  }, []);
 
-  return fromHash(inputAsHash, context, parentInlinerContext, callback);
+  return fromStyles(inputAsImports.join(''), context, { processImports: true }, callback);
 }
 
-function fromHash(input, context, parentInlinerContext, callback) {
-  var tokens = [];
-  var newTokens = [];
+function fromHash(input, context, callback) {
   var uri;
-  var realUri;
   var source;
-  var parsedMap;
-  var rebasedMap;
-  var rebaseConfig;
+  var inputAsImports = [];
 
   for (uri in input) {
     source = input[uri];
-    realUri = uri == UNKNOWN_URI ? undefined : uri;
+    inputAsImports.push(restoreAsRelativeImport(uri));
+
+    context.sourcesContent[uri] = source.styles;
 
     if (source.sourceMap) {
-      parsedMap = typeof source.sourceMap == 'string' ?
-        JSON.parse(source.sourceMap) :
-        source.sourceMap;
-      rebasedMap = isRemoteResource(uri) ?
-        rebaseRemoteMap(parsedMap, uri) :
-        rebaseLocalMap(parsedMap, uri, context.options.rebaseTo);
-      context.inputSourceMapTracker.track(realUri, rebasedMap);
+      trackSourceMap(source.sourceMap, uri, context);
     }
-
-    context.sourcesContent[realUri] = source.styles;
   }
 
-  for (uri in input) {
-    source = input[uri];
-    realUri = uri == UNKNOWN_URI ? undefined : uri;
-    rebaseConfig = {};
-
-    if (uri == UNKNOWN_URI) {
-      rebaseConfig.fromBase = path.resolve('');
-      rebaseConfig.toBase = context.options.rebaseTo;
-    } else if (isRemoteResource(uri)) {
-      rebaseConfig.fromBase = uri;
-      rebaseConfig.toBase = uri;
-    } else if (isAbsoluteResource(uri)) {
-      rebaseConfig.fromBase = path.dirname(uri);
-      rebaseConfig.toBase = context.options.rebaseTo;
-    } else {
-      rebaseConfig.fromBase = path.dirname(path.resolve(uri));
-      rebaseConfig.toBase = context.options.rebaseTo;
-    }
+  return fromStyles(inputAsImports.join(''), context, { processImports: true }, callback);
+}
 
-    context.source = realUri;
+function trackSourceMap(sourceMap, uri, context) {
+  var parsedMap = typeof sourceMap == 'string' ?
+      JSON.parse(sourceMap) :
+      sourceMap;
+  var rebasedMap = isRemoteResource(uri) ?
+    rebaseRemoteMap(parsedMap, uri) :
+    rebaseLocalMap(parsedMap, uri || UNKNOWN_URI, context.options.rebaseTo);
 
-    newTokens = tokenize(source.styles, context);
-    newTokens = rebase(newTokens, context.options.rebase, context.validator, rebaseConfig);
+  context.inputSourceMapTracker.track(uri, rebasedMap);
+}
 
-    tokens = tokens.concat(newTokens);
+function restoreAsRelativeImport(uri) {
+  var currentPath = path.resolve('');
+  var absoluteUri;
+  var relativeToCurrentPath;
+
+  if (isRemoteResource(uri)) {
+    return restoreImport(uri, '') + Marker.SEMICOLON;
+  } else {
+    absoluteUri = isAbsoluteResource(uri) ?
+      uri :
+      path.resolve(uri);
+    relativeToCurrentPath = path.relative(currentPath, absoluteUri);
 
-    context.stats.originalSize += source.styles.length;
+    return restoreImport(relativeToCurrentPath, '') + Marker.SEMICOLON;
   }
+}
 
-  return context.options.processImport ?
+function fromStyles(styles, context, parentInlinerContext, callback) {
+  var tokens;
+  var rebaseConfig = {};
+
+  if (!context.source) {
+    rebaseConfig.fromBase = path.resolve('');
+    rebaseConfig.toBase = context.options.rebaseTo;
+  } else if (isRemoteResource(context.source)) {
+    rebaseConfig.fromBase = context.source;
+    rebaseConfig.toBase = context.source;
+  } else if (isAbsoluteResource(context.source)) {
+    rebaseConfig.fromBase = path.dirname(context.source);
+    rebaseConfig.toBase = context.options.rebaseTo;
+  } else {
+    rebaseConfig.fromBase = path.dirname(path.resolve(context.source));
+    rebaseConfig.toBase = context.options.rebaseTo;
+  }
+
+  tokens = tokenize(styles, context);
+  tokens = rebase(tokens, context.options.rebase, context.validator, rebaseConfig);
+
+  return parentInlinerContext.processImports ?
     inlineImports(tokens, context, parentInlinerContext, callback) :
     callback(tokens);
 }
@@ -153,6 +147,7 @@ function inlineImports(tokens, externalContext, parentInlinerContext, callback)
     isRemote: parentInlinerContext.isRemote || false,
     localOnly: externalContext.localOnly,
     outputTokens: [],
+    processImports: parentInlinerContext.processImports,
     processImportFrom: externalContext.options.processImportFrom,
     rebaseTo: externalContext.options.rebaseTo,
     sourceTokens: tokens,
@@ -198,7 +193,7 @@ function inlineStylesheet(token, inlinerContext) {
 function inlineRemoteStylesheet(uri, mediaQuery, metadata, inlinerContext) {
   var isAllowed = isAllowedResource(uri, true, inlinerContext.processImportFrom);
   var originalUri = uri;
-  var isLoaded = isAlreadyLoaded(uri, inlinerContext.externalContext.sourcesContent);
+  var isLoaded = uri in inlinerContext.externalContext.sourcesContent;
 
   if (inlinerContext.inlinedStylesheets.indexOf(uri) > -1) {
     inlinerContext.warnings.push('Ignoring remote @import of "' + uri + '" as it has already been imported.');
@@ -227,8 +222,6 @@ function inlineRemoteStylesheet(uri, mediaQuery, metadata, inlinerContext) {
   inlinerContext.inlinedStylesheets.push(uri);
 
   function whenLoaded(error, importedStyles) {
-    var sourceHash = {};
-
     if (error) {
       inlinerContext.errors.push('Broken @import declaration of "' + uri + '" - ' + error);
 
@@ -239,19 +232,20 @@ function inlineRemoteStylesheet(uri, mediaQuery, metadata, inlinerContext) {
       });
     }
 
-    sourceHash[originalUri] = {
-      styles: importedStyles
-    };
+    inlinerContext.processImports = inlinerContext.externalContext.options.processImport;
+    inlinerContext.isRemote = true;
 
+    inlinerContext.externalContext.source = originalUri;
     inlinerContext.externalContext.sourcesContent[uri] = importedStyles;
-    inlinerContext.isRemote = true;
-    fromHash(sourceHash, inlinerContext.externalContext, inlinerContext, function (importedTokens) {
+    inlinerContext.externalContext.stats.originalSize += importedStyles.length;
+
+    return fromStyles(importedStyles, inlinerContext.externalContext, inlinerContext, function (importedTokens) {
       importedTokens = wrapInMedia(importedTokens, mediaQuery, metadata);
 
       inlinerContext.outputTokens = inlinerContext.outputTokens.concat(importedTokens);
       inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
 
-      doInlineImports(inlinerContext);
+      return doInlineImports(inlinerContext);
     });
   }
 
@@ -269,12 +263,11 @@ function inlineLocalStylesheet(uri, mediaQuery, metadata, inlinerContext) {
   var importedStyles;
   var importedTokens;
   var isAllowed = isAllowedResource(uri, false, inlinerContext.processImportFrom);
-  var sourceHash = {};
-  var isLoaded = isAlreadyLoaded(relativeToCurrentPath, inlinerContext.externalContext.sourcesContent);
+  var isLoaded = relativeToCurrentPath in inlinerContext.externalContext.sourcesContent;
 
   if (inlinerContext.inlinedStylesheets.indexOf(absoluteUri) > -1) {
     inlinerContext.warnings.push('Ignoring local @import of "' + uri + '" as it has already been imported.');
-  } else if (!isLoaded && !fs.existsSync(absoluteUri) || !fs.statSync(absoluteUri).isFile()) {
+  } else if (!isLoaded && (!fs.existsSync(absoluteUri) || !fs.statSync(absoluteUri).isFile())) {
     inlinerContext.errors.push('Ignoring local @import of "' + uri + '" as resource is missing.');
   } else if (!isAllowed && inlinerContext.afterContent) {
     inlinerContext.warnings.push('Ignoring local @import of "' + uri + '" as resource is not allowed and after other content.');
@@ -287,14 +280,15 @@ function inlineLocalStylesheet(uri, mediaQuery, metadata, inlinerContext) {
     importedStyles = isLoaded ?
       inlinerContext.externalContext.sourcesContent[relativeToCurrentPath] :
       fs.readFileSync(absoluteUri, 'utf-8');
+
     inlinerContext.inlinedStylesheets.push(absoluteUri);
+    inlinerContext.processImports = inlinerContext.externalContext.options.processImport;
 
-    sourceHash[relativeToCurrentPath] = {
-      styles: importedStyles
-    };
+    inlinerContext.externalContext.source = relativeToCurrentPath;
     inlinerContext.externalContext.sourcesContent[relativeToCurrentPath] = importedStyles;
+    inlinerContext.externalContext.stats.originalSize += importedStyles.length;
 
-    importedTokens = fromHash(sourceHash, inlinerContext.externalContext, inlinerContext, function (tokens) { return tokens; });
+    importedTokens = fromStyles(importedStyles, inlinerContext.externalContext, inlinerContext, function (tokens) { return tokens; });
     importedTokens = wrapInMedia(importedTokens, mediaQuery, metadata);
 
     inlinerContext.outputTokens = inlinerContext.outputTokens.concat(importedTokens);
index 33c0dfc..6fff4dd 100644 (file)
@@ -627,18 +627,20 @@ vows.describe('module tests').addBatch({
       }
     },
     'with remote paths': {
-      'topic': new CleanCSS().minify({
-        'http://127.0.0.1/styles.css': {
-          styles: 'div{background-image:url(image.png)}'
-        }
-      }),
+      'topic': function() {
+        return new CleanCSS().minify({
+          'http://127.0.0.1/styles.css': {
+            styles: 'div{background-image:url(image.png)}'
+          }
+        });
+      },
       'gives right output': function (minified) {
         assert.equal(minified.styles, 'div{background-image:url(http://127.0.0.1/image.png)}');
       }
     },
     'with already resolved imports': {
       'topic': function () {
-        new CleanCSS().minify({
+        new CleanCSS({ advanced: false }).minify({
           'main.css': {
             styles: '@import url(test/fixtures/partials/one.css);\n@import url(http://127.0.0.1/test.css);'
           },