Merge branch 'devel' into sso

Conflicts:
	tools/bundler.js
This commit is contained in:
David Glasser
2014-02-07 11:59:21 -08:00
28 changed files with 2885 additions and 104 deletions

View File

@@ -1,5 +1,14 @@
## v.NEXT
* XXX upgraded `less` from 1.3.3 to 1.6.1
* XXX upgraded `stylus` from 0.37.0 to 0.42.2 and `nib` from `1.0.0` to `1.0.2`
* XXX sourcemaps support for stylesheets, including less sourcemaps
* XXX css linting (breaks on errors)
* XXX css preprocessing to concatenate files correctly (pulls @imports to the
beginning)
* XXX supports `.import.less` and `.import.styl` to prevent Meteor processing
stylesheets. `.lessimport` is deprecated
* Hash login tokens before storing them in the database.
* Add `clientAddress` and `httpHeaders` to `this.connection` in method

View File

@@ -71,13 +71,6 @@ handlebars: https://github.com/wycats/handlebars.js/
Copyright (C) 2011 by Yehuda Katz
----------
clean-css: https://github.com/GoalSmashers/clean-css
----------
Copyright (c) 2011 GoalSmashers.com
----------
progress: https://github.com/visionmedia/node-progress
qs: https://github.com/visionmedia/node-querystring
@@ -91,11 +84,14 @@ pause: https://github.com/visionmedia/node-pause
range-parser: https://github.com/visionmedia/node-range-parser
send: https://github.com/visionmedia/send
methods: https://github.com/visionmedia/node-methods
css-parse: https://github.com/reworkcss/css-parse
css-stringify: https://github.com/reworkcss/css-stringify
----------
Copyright (c) 2010 TJ Holowaychuk <tj@vision-media.ca>
Copyright (c) 2011 TJ Holowaychuk <tj@vision-media.ca>
Copyright (c) 2012 TJ Holowaychuk <tj@vision-media.ca>
Copyright (c) 2013 TJ Holowaychuk <tj@vision-media.ca>
----------

View File

@@ -11,7 +11,7 @@ automatically compiled to CSS and the results are included in the client CSS
bundle.
{{#note}}
If you want to `@import` a file, give it the extension `.lessimport`
If you want to `@import` a file, give it the extension `.import.less`
to prevent Meteor from processing it independently.
{{/note}}

View File

@@ -14,6 +14,11 @@ The `stylus` package also includes `nib` support. Add `@import 'nib'` to
your `.styl` files to enable cross-browser mixins such as
`linear-gradient` and `border-radius`.
{{#note}}
If you want to `@import` a file, give it the extension `.import.styl`
to prevent Meteor from processing it independently.
{{/note}}
See <http://visionmedia.github.com/nib> for documentation of the nib extensions of Stylus.
{{/better_markdown}}

4
meteor
View File

@@ -1,9 +1,5 @@
#!/bin/bash
# danger will robinson! mother:config/download-dev-bundles.sh only goes up to
# 0.3.30!
# Before you increment this again, fix the script listed above.
BUNDLE_VERSION=0.3.30
# OS Check. Put here because here is where we download the precompiled

View File

@@ -1,10 +1,110 @@
{
"dependencies": {
"less": {
"version": "1.3.3",
"version": "1.6.1",
"dependencies": {
"ycssmin": {
"version": "1.0.1"
"mime": {
"version": "1.2.11"
},
"request": {
"version": "2.33.0",
"dependencies": {
"qs": {
"version": "0.6.6"
},
"json-stringify-safe": {
"version": "5.0.0"
},
"forever-agent": {
"version": "0.5.0"
},
"node-uuid": {
"version": "1.4.1"
},
"tough-cookie": {
"version": "0.12.1",
"dependencies": {
"punycode": {
"version": "1.2.3"
}
}
},
"form-data": {
"version": "0.1.2",
"dependencies": {
"combined-stream": {
"version": "0.0.4",
"dependencies": {
"delayed-stream": {
"version": "0.0.5"
}
}
},
"async": {
"version": "0.2.10"
}
}
},
"tunnel-agent": {
"version": "0.3.0"
},
"http-signature": {
"version": "0.10.0",
"dependencies": {
"assert-plus": {
"version": "0.1.2"
},
"asn1": {
"version": "0.1.11"
},
"ctype": {
"version": "0.5.2"
}
}
},
"oauth-sign": {
"version": "0.3.0"
},
"hawk": {
"version": "1.0.0",
"dependencies": {
"hoek": {
"version": "0.9.1"
},
"boom": {
"version": "0.4.2"
},
"cryptiles": {
"version": "0.2.2"
},
"sntp": {
"version": "0.2.4"
}
}
},
"aws-sign2": {
"version": "0.5.0"
}
}
},
"mkdirp": {
"version": "0.3.5"
},
"clean-css": {
"version": "2.0.7",
"dependencies": {
"commander": {
"version": "2.0.0"
}
}
},
"source-map": {
"version": "0.1.31",
"dependencies": {
"amdefine": {
"version": "0.1.0"
}
}
}
}
}

2
packages/less/less_tests.import.less vendored Normal file
View File

@@ -0,0 +1,2 @@
// This should be @import'able into other .less files
@important-border: double;

View File

@@ -10,6 +10,7 @@ Tinytest.add("less - presence", function(test) {
// test @import
test.equal(getStyleProperty(p, 'border-right-style'), "dotted");
test.equal(getStyleProperty(p, 'border-bottom-style'), "double");
d.kill();
});

View File

@@ -1,4 +1,5 @@
@import "less_tests_constants.lessimport";
@import "less_tests.import.less";
#less-tests { zoom: 1; /* prop this rule open */ }
@@ -7,4 +8,5 @@
.less-dashy-left-border {
border-left: @dashy;
border-right: @external-dotty;
border-bottom: @important-border;
}

View File

@@ -8,11 +8,12 @@ Package._transitional_registerBuildPlugin({
sources: [
'plugin/compile-less.js'
],
npmDependencies: {"less": "1.3.3"}
npmDependencies: {"less": "1.6.1"}
});
Package.on_test(function (api) {
api.use(['test-helpers', 'tinytest', 'less']);
api.use(['spark']);
api.add_files(['less_tests.less', 'less_tests.js'], 'client');
api.add_files(['less_tests.less', 'less_tests.js', 'less_tests.import.less'],
'client');
});

View File

@@ -23,13 +23,14 @@ Plugin.registerSourceHandler("less", function (compileStep) {
paths: [path.dirname(compileStep._fullInputPath)] // for @import
};
var f = new Future;
var css;
var parser = new less.Parser(options);
var astFuture = new Future;
var ast;
try {
less.render(source, options, f.resolver());
css = f.wait();
parser.parse(source, astFuture.resolver());
ast = astFuture.wait();
} catch (e) {
// less.render() is supposed to report any errors via its
// less.Parser.parse is supposed to report any errors via its
// callback. But sometimes, it throws them instead. This is
// probably a bug in less. Be prepared for either behavior.
compileStep.error({
@@ -41,14 +42,31 @@ Plugin.registerSourceHandler("less", function (compileStep) {
return;
}
var cssFuture = new Future;
var css = ast.toCSS({
sourceMap: Boolean(true),
writeSourceMap: function (sourceMap) {
cssFuture.return(sourceMap);
}
});
var sourceMap = JSON.parse(cssFuture.wait());
sourceMap.sources = [compileStep.inputPath];
sourceMap.sourcesContent = [source];
compileStep.addStylesheet({
path: compileStep.inputPath + ".css",
data: css
data: css,
sourceMap: JSON.stringify(sourceMap)
});
});;
// Register lessimport files with the dependency watcher, without actually
// processing them.
Plugin.registerSourceHandler("lessimport", function () {
// Register import.less files with the dependency watcher, without actually
// processing them. There is a similar rule in the stylus package.
Plugin.registerSourceHandler("import.less", function () {
// Do nothing
});
// Backward compatibility with Meteor 0.7
Plugin.registerSourceHandler("lessimport", function () {});

View File

@@ -1,13 +1,5 @@
{
"dependencies": {
"clean-css": {
"version": "2.0.2",
"dependencies": {
"commander": {
"version": "2.0.0"
}
}
},
"uglify-js": {
"version": "2.4.7",
"dependencies": {
@@ -34,6 +26,22 @@
"version": "1.0.1"
}
}
},
"css-parse": {
"version": "1.7.0"
},
"css-stringify": {
"version": "1.4.1",
"dependencies": {
"source-map": {
"version": "0.1.31",
"dependencies": {
"amdefine": {
"version": "0.1.0"
}
}
}
}
}
}
}

View File

@@ -0,0 +1,137 @@
// Stringifier based on css-stringify
var emit = function (str) {
return str.toString();
};
var visit = function (node, last) {
return traverse[node.type](node, last);
};
var mapVisit = function (nodes) {
var buf = "";
for (var i = 0, length = nodes.length; i < length; i++) {
buf += visit(nodes[i], i === length - 1);
}
return buf;
};
MinifyAst = function(node) {
return node.stylesheet
.rules.map(function (rule) { return visit(rule); })
.join('');
};
var traverse = {};
traverse.comment = function(node) {
return emit('', node.position);
};
traverse.import = function(node) {
return emit('@import ' + node.import + ';', node.position);
};
traverse.media = function(node) {
return emit('@media ' + node.media, node.position, true)
+ emit('{')
+ mapVisit(node.rules)
+ emit('}');
};
traverse.document = function(node) {
var doc = '@' + (node.vendor || '') + 'document ' + node.document;
return emit(doc, node.position, true)
+ emit('{')
+ mapVisit(node.rules)
+ emit('}');
};
traverse.charset = function(node) {
return emit('@charset ' + node.charset + ';', node.position);
};
traverse.namespace = function(node) {
return emit('@namespace ' + node.namespace + ';', node.position);
};
traverse.supports = function(node){
return emit('@supports ' + node.supports, node.position, true)
+ emit('{')
+ mapVisit(node.rules)
+ emit('}');
};
traverse.keyframes = function(node) {
return emit('@'
+ (node.vendor || '')
+ 'keyframes '
+ node.name, node.position, true)
+ emit('{')
+ mapVisit(node.keyframes)
+ emit('}');
};
traverse.keyframe = function(node) {
var decls = node.declarations;
return emit(node.values.join(','), node.position, true)
+ emit('{')
+ mapVisit(decls)
+ emit('}');
};
traverse.page = function(node) {
var sel = node.selectors.length
? node.selectors.join(', ')
: '';
return emit('@page ' + sel, node.position, true)
+ emit('{')
+ mapVisit(node.declarations)
+ emit('}');
};
traverse.rule = function(node) {
var decls = node.declarations;
if (!decls.length) return '';
var selectors = node.selectors.map(function (selector) {
// removes universal selectors like *.class => .class
// removes optional whitespace around '>' and '+'
return selector.replace(/\*\./, '.')
.replace(/\s*>\s*/g, '>')
.replace(/\s*\+\s*/g, '+');
});
return emit(selectors.join(','), node.position, true)
+ emit('{')
+ mapVisit(decls)
+ emit('}');
};
traverse.declaration = function(node, last) {
var value = node.value;
// remove optional quotes around font name
if (node.property === 'font') {
value = value.replace(/\'[^\']+\'/g, function (m) {
if (m.indexOf(' ') !== -1)
return m;
return m.replace(/\'/g, '');
});
value = value.replace(/\"[^\"]+\"/g, function (m) {
if (m.indexOf(' ') !== -1)
return m;
return m.replace(/\"/g, '');
});
}
// remove url quotes if possible
// in case it is the last declaration, we can omit the semicolon
return emit(node.property + ':' + value, node.position)
+ (last ? '' : emit(';'));
};

View File

@@ -0,0 +1,21 @@
Tinytest.add("minifiers - simple css minification", function (test) {
var t = function (css, expected, desc) {
test.equal(CssTools.minifyCss(css), expected, desc);
}
t('a \t\n{ color: red } \n', 'a{color:red}', 'whitespace check');
t('a \t\n{ color: red; margin: 1; } \n', 'a{color:red;margin:1}', 'only last one loses semicolon');
t('a \t\n{ color: red;;; margin: 1;;; } \n', 'a{color:red;margin:1}', 'more semicolons than needed');
t('a , p \t\n{ color: red; } \n', 'a,p{color:red}', 'multiple selectors');
t('body {}', '', 'removing empty rules');
t('*.my-class { color: #fff; }', '.my-class{color:#fff}', 'removing universal selector');
t('p > *.my-class { color: #fff; }', 'p>.my-class{color:#fff}', 'removing optional whitespace around ">" in selector');
t('p + *.my-class { color: #fff; }', 'p+.my-class{color:#fff}', 'removing optional whitespace around "+" in selector');
// XXX url parsing is difficult to support at the moment
t('a {\n\
font:12px \'Helvetica\',"Arial",\'Nautica\';\n\
background:url("/some/nice/picture.png");\n}',
'a{font:12px Helvetica,Arial,Nautica;background:url("/some/nice/picture.png")}', 'removing quotes in font and url (if possible)');
t('/* no comments */ a { color: red; }', 'a{color:red}', 'remove comments');
});

View File

@@ -1,8 +1,76 @@
var CleanCss = Npm.require('clean-css');
UglifyJSMinify = Npm.require('uglify-js').minify;
CleanCSSProcess = function (source, options) {
var instance = new CleanCss(options);
return instance.minify(source);
var cssParse = Npm.require('css-parse');
var cssStringify = Npm.require('css-stringify');
CssTools = {
parseCss: cssParse,
stringifyCss: cssStringify,
minifyCss: function (cssText) {
return CssTools.minifyCssAst(cssParse(cssText));
},
minifyCssAst: function (cssAst) {
return MinifyAst(cssAst);
},
mergeCssAsts: function (cssAsts, warnCb) {
var rulesPredicate = function (rules) {
if (! _.isArray(rules))
rules = [rules];
return function (node) {
return _.contains(rules, node.type);
}
};
// Simple concatenation of CSS files would break @import rules
// located in the beginning of a file. Before concatenation, pull them to
// the beginning of a new syntax tree so they always precede other rules.
var newAst = {
type: 'stylesheet',
stylesheet: { rules: [] }
};
_.each(cssAsts, function (ast) {
// Pick only the imports from the beginning of file ignoring @charset
// rules as every file is assumed to be in UTF-8.
var charsetRules = _.filter(ast.stylesheet.rules,
rulesPredicate("charset"));
if (_.any(charsetRules, function (rule) {
// According to MDN, only 'UTF-8' and "UTF-8" are the correct encoding
// directives representing UTF-8.
return ! /^(['"])UTF-8\1$/.test(rule.charset);
})) {
warnCb(ast.filename, "@charset rules in this file will be ignored as UTF-8 is the only encoding supported");
}
ast.stylesheet.rules = _.reject(ast.stylesheet.rules,
rulesPredicate("charset"));
var importCount = 0;
for (var i = 0; i < ast.stylesheet.rules.length; i++)
if (! rulesPredicate(["import", "comment"])(ast.stylesheet.rules[i])) {
importCount = i;
break;
}
var imports = ast.stylesheet.rules.splice(0, importCount);
newAst.stylesheet.rules = newAst.stylesheet.rules.concat(imports);
// if there are imports left in the middle of file, warn user as it might
// be a potential bug (imports are valid only in the beginning of file).
if (_.any(ast.stylesheet.rules, rulesPredicate("import"))) {
// XXX make this an error?
warnCb(ast.filename, "there are some @import rules those are not taking effect as they are required to be in the beginning of the file");
}
});
// Now we can put the rest of CSS rules into new AST
_.each(cssAsts, function (ast) {
newAst.stylesheet.rules =
newAst.stylesheet.rules.concat(ast.stylesheet.rules);
});
return newAst;
}
};
UglifyJSMinify = Npm.require('uglify-js').minify;

View File

@@ -4,11 +4,20 @@ Package.describe({
});
Npm.depends({
"clean-css": "2.0.2",
"uglify-js": "2.4.7"
"uglify-js": "2.4.7",
"css-parse": "1.7.0",
"css-stringify": "1.4.1"
});
Package.on_use(function (api) {
api.export(['CleanCSSProcess', 'UglifyJSMinify']);
api.add_files('minifiers.js', 'server');
api.use('underscore', 'server');
api.export(['CssTools', 'UglifyJSMinify']);
api.add_files(['minification.js', 'minifiers.js'], 'server');
});
Package.on_test(function (api) {
api.use('minifiers', 'server');
api.use('tinytest');
api.add_files('minifiers-tests.js', 'server');
});

View File

@@ -1,27 +1,46 @@
{
"dependencies": {
"stylus": {
"version": "0.37.0",
"version": "0.42.2",
"dependencies": {
"cssom": {
"version": "0.2.5"
"css-parse": {
"version": "1.7.0"
},
"mkdirp": {
"version": "0.3.5"
},
"debug": {
"version": "0.7.2"
"version": "0.7.4"
},
"sax": {
"version": "0.5.4"
"version": "0.5.8"
},
"glob": {
"version": "3.2.8",
"dependencies": {
"minimatch": {
"version": "0.2.14",
"dependencies": {
"lru-cache": {
"version": "2.5.0"
},
"sigmund": {
"version": "1.0.0"
}
}
},
"inherits": {
"version": "2.0.1"
}
}
}
}
},
"nib": {
"version": "1.0.0",
"version": "1.0.2",
"dependencies": {
"stylus": {
"version": "0.34.1",
"version": "0.37.0",
"dependencies": {
"cssom": {
"version": "0.2.5"
@@ -30,7 +49,10 @@
"version": "0.3.5"
},
"debug": {
"version": "0.7.2"
"version": "0.7.4"
},
"sax": {
"version": "0.5.8"
}
}
}

View File

@@ -8,11 +8,15 @@ Package._transitional_registerBuildPlugin({
sources: [
'plugin/compile-stylus.js'
],
npmDependencies: { stylus: "0.37.0", nib: "1.0.0" }
npmDependencies: { stylus: "0.42.2", nib: "1.0.2" }
});
Package.on_test(function (api) {
api.use(['tinytest', 'stylus', 'test-helpers']);
api.use('spark');
api.add_files(['stylus_tests.styl', 'stylus_tests.js'], 'client');
api.add_files([
'stylus_tests.styl',
'stylus_tests.import.styl',
'stylus_tests.js'
],'client');
});

View File

@@ -1,6 +1,7 @@
var fs = Npm.require('fs');
var stylus = Npm.require('stylus');
var nib = Npm.require('nib');
var path = Npm.require('path');
var Future = Npm.require('fibers/future');
Plugin.registerSourceHandler("styl", function (compileStep) {
@@ -17,6 +18,8 @@ Plugin.registerSourceHandler("styl", function (compileStep) {
stylus(compileStep.read().toString('utf8'))
.use(nib())
.set('filename', compileStep.inputPath)
// Include needed to allow relative @imports in stylus files
.include(path.dirname(compileStep._fullInputPath))
.render(f.resolver());
try {
@@ -32,3 +35,10 @@ Plugin.registerSourceHandler("styl", function (compileStep) {
data: css
});
});
// Register import.styl files with the dependency watcher, without actually
// processing them. There is a similar rule in the less package.
Plugin.registerSourceHandler("import.styl", function () {
// Do nothing
});

View File

@@ -0,0 +1,5 @@
// Variable used in stylus_test.styl
importDashy = dashed
.stylus-overwrite-color
font-size: 20px !important

View File

@@ -1,6 +1,5 @@
Tinytest.add("stylus - presence", function(test) {
var d = OnscreenDiv(Meteor.render(function() {
return '<p class="stylus-dashy-left-border"></p>'; }));
d.node().style.display = 'block';
@@ -10,5 +9,17 @@ Tinytest.add("stylus - presence", function(test) {
test.equal(leftBorder, "dashed");
d.kill();
});
Tinytest.add("stylus - @import", function(test) {
var d = OnscreenDiv(Meteor.render(function() {
return '<p class="stylus-import-dashy-border stylus-overwrite-color"></p>';
}));
d.node().style.display = 'block';
var p = d.node().firstChild;
test.equal(getStyleProperty(p, 'font-size'), "20px");
test.equal(getStyleProperty(p, 'border-left-style'), "dashed");
d.kill();
});

View File

@@ -1,3 +1,4 @@
@import "stylus_tests.import.styl"
#stylus-tests
zoom: 1
@@ -7,3 +8,9 @@ dashy = dashed
.stylus-dashy-left-border
border-left: 1px dashy black
.stylus-overwrite-size
// This property is overwritten in stylus_test.import.styl
font-size: 10px
.stylus-import-dashy-border
border-left: 1px importDashy black

File diff suppressed because it is too large Load Diff

View File

@@ -132,3 +132,18 @@ body {
top: 10px;
z-index: 2;
}
.string_equal {
line-height: 1.2;
margin-left: 30px;
}
.string_equal ins {
text-decoration: none;
}
.string_equal_expected ins {
background: #fe0;
}
.string_equal_actual ins {
color: #d00;
background: #fcc;
}

View File

@@ -458,46 +458,81 @@ Template.event.events({
}
});
Template.event.get_details = function() {
var prepare = function(details) {
return _.compact(_.map(details, function(val, key) {
// You can end up with a an undefined value, e.g. using
// isNull without providing a message attribute: isNull(1).
// No need to display those.
if (!_.isUndefined(val)) {
return {
key: key,
val: val
};
} else {
return undefined;
}
}));
};
var details = this.details;
if (! details) {
return null;
} else {
var type = details.type;
var stack = details.stack;
details = _.clone(details);
delete details.type;
delete details.stack;
return {
type: type,
stack: stack,
details: prepare(details)
};
}
// e.g. doDiff('abc', 'bcd') => [[-1, 'a'], [0, 'bc'], [1, 'd']]
var doDiff = function (str1, str2) {
var D = new diff_match_patch();
var pieces = D.diff_main(str1, str2, false);
D.diff_cleanupSemantic(pieces);
return pieces;
};
Template.event.helpers({
get_details: function() {
var details = this.details;
if (! details) {
return null;
} else {
var type = details.type;
var stack = details.stack;
details = _.clone(details);
delete details.type;
delete details.stack;
var prepare = function(details) {
if (type === 'string_equal') {
var diff = doDiff(details.actual,
details.expected);
}
return _.compact(_.map(details, function(val, key) {
// make test._stringEqual results print nicely,
// in particular for multiline strings
if (type === 'string_equal' &&
(key === 'actual' || key === 'expected')) {
var html = '<pre class="string_equal string_equal_'+key+'">';
_.each(diff, function (piece) {
var which = piece[0];
var text = piece[1];
if (which === 0 ||
which === (key === 'actual' ? -1 : 1)) {
var htmlBit = Handlebars._escape(text).replace(
/\n/g, '<br>');
if (which !== 0)
htmlBit = '<ins>' + htmlBit + '</ins>';
html += htmlBit;
}
});
html += '</pre>';
val = new Handlebars.SafeString(html);
}
// You can end up with a an undefined value, e.g. using
// isNull without providing a message attribute: isNull(1).
// No need to display those.
if (!_.isUndefined(val)) {
return {
key: key,
val: val
};
} else {
return undefined;
}
}));
};
return {
type: type,
stack: stack,
details: prepare(details)
};
}
}
});
Template.event.is_debuggable = function() {
return !!this.cookie;
};

View File

@@ -16,6 +16,8 @@ Package.on_use(function (api) {
api.use(['spark', 'livedata', 'templating', 'deps'], 'client');
api.add_files('diff_match_patch_uncompressed.js', 'client');
api.add_files([
'driver.css',
'driver.html',

View File

@@ -107,6 +107,13 @@ _.extend(TestCaseResults.prototype, {
// XXX eliminate 'message' and 'not' arguments
equal: function (actual, expected, message, not) {
if ((! not) && (typeof actual === 'string') &&
(typeof expected === 'string')) {
this._stringEqual(actual, expected, message);
return;
}
/* If expected is a DOM node, do a literal '===' comparison with
* actual. Otherwise do a deep comparison, as implemented by _.isEqual.
*/
@@ -162,7 +169,7 @@ _.extend(TestCaseResults.prototype, {
},
// XXX nodejs assert.throws can take an expected error, as a class,
// regular expression, or predicate function. However, with its
// regular expression, or predicate function. However, with its
// implementation if a constructor (class) is passed in and `actual`
// fails the instanceof test, the constructor is then treated as
// a predicate and called with `actual` (!)
@@ -269,8 +276,23 @@ _.extend(TestCaseResults.prototype, {
else
this.fail({type: "length", expected: expected_length,
actual: obj.length});
},
// EXPERIMENTAL way to compare two strings that results in
// a nicer display in the test runner, e.g. for multiline
// strings
_stringEqual: function (actual, expected, message) {
if (actual !== expected) {
this.fail({type: "string_equal",
message: message,
expected: expected,
actual: actual});
} else {
this.ok();
}
}
});
/******************************************************************************/

View File

@@ -159,9 +159,9 @@
// wait until later.
var path = require('path');
var util = require('util');
var files = require(path.join(__dirname, 'files.js'));
var packages = require(path.join(__dirname, 'packages.js'));
var linker = require(path.join(__dirname, 'linker.js'));
var Builder = require(path.join(__dirname, 'builder.js'));
var archinfo = require(path.join(__dirname, 'archinfo.js'));
var buildmessage = require('./buildmessage.js');
@@ -457,6 +457,11 @@ _.extend(Target.prototype, {
// Link JavaScript and set up self.js, etc.
self._emitResources();
// Preprocess and concatenate CSS files for client targets.
if (self instanceof ClientTarget) {
self.mergeCss();
}
// Minify, if requested
if (options.minify) {
var minifiers = unipackage.load({
@@ -464,8 +469,11 @@ _.extend(Target.prototype, {
packages: ['minifiers']
}).minifiers;
self.minifyJs(minifiers);
if (self.minifyCss) // XXX a bit of a hack
// CSS is minified only for client targets.
if (self instanceof ClientTarget) {
self.minifyCss(minifiers);
}
}
if (options.addCacheBusters) {
@@ -669,7 +677,8 @@ _.extend(Target.prototype, {
}
}
if (resource.type === "js" && resource.sourceMap) {
// Both CSS and JS files can have source maps
if (resource.sourceMap) {
f.setSourceMap(resource.sourceMap, path.dirname(relPath));
}
@@ -753,9 +762,11 @@ var ClientTarget = function (options) {
var self = this;
Target.apply(this, arguments);
// CSS files. List of File. They will be loaded at page load in the
// order given.
// CSS files. List of File. They will be loaded in the order given.
self.css = [];
// Cached CSS AST. If non-null, self.css has one item in it, processed CSS
// from merged input files, and this is its parse tree.
self._cssAstCache = null;
// List of segments of additional HTML for <head>/<body>.
self.head = [];
@@ -768,17 +779,88 @@ var ClientTarget = function (options) {
inherits(ClientTarget, Target);
_.extend(ClientTarget.prototype, {
// Lints CSS files and merges them into one file, fixing up source maps and
// pulling any @import directives up to the top since the CSS spec does not
// allow them to appear in the middle of a file.
mergeCss: function () {
var self = this;
var minifiers = unipackage.load({
library: self.library,
packages: ['minifiers']
}).minifiers;
var CssTools = minifiers.CssTools;
// Filenames passed to AST manipulator mapped to their original files
var originals = {};
var cssAsts = _.map(self.css, function (file) {
var filename = file.url.replace(/^\//, '');
originals[filename] = file;
try {
var parseOptions = { source: filename, position: true };
var ast = CssTools.parseCss(file.contents('utf8'), parseOptions);
ast.filename = filename;
} catch (e) {
buildmessage.error(e.message, { file: filename });
return { type: "stylesheet", stylesheet: { rules: [] },
filename: filename };
}
return ast;
});
var warnCb = function (filename, msg) {
// XXX make this a buildmessage.warning call rather than a random log
console.log("%s: warn: %s", filename, msg);
};
// Other build phases might need this AST later
self._cssAstCache = CssTools.mergeCssAsts(cssAsts, warnCb);
// Overwrite the CSS files list with the new concatenated file
var stringifiedCss = CssTools.stringifyCss(self._cssAstCache,
{ sourcemap: true });
self.css = [new File({ data: new Buffer(stringifiedCss.code, 'utf8') })];
// Add the contents of the input files to the source map of the new file
stringifiedCss.map.sourcesContent =
_.map(stringifiedCss.map.sources, function (filename) {
return originals[filename].contents('utf8');
});
// If any input files had source maps, apply them.
// Ex.: less -> css source map should be composed with css -> css source map
var newMap = sourcemap.SourceMapGenerator.fromSourceMap(
new sourcemap.SourceMapConsumer(stringifiedCss.map));
_.each(originals, function (file, name) {
if (! file.sourceMap)
return;
newMap.applySourceMap(
new sourcemap.SourceMapConsumer(file.sourceMap), name);
});
self.css[0].setSourceMap(JSON.stringify(newMap));
self.css[0].setUrlToHash(".css");
},
// Minify the CSS in this target
minifyCss: function (minifiers) {
var self = this;
var minifiedCss = '';
var allCss = _.map(self.css, function (file) {
return file.contents('utf8');
}).join('\n');
// If there is an AST already calculated, don't waste time on parsing it
// again.
if (self._cssAstCache) {
minifiedCss = minifiers.CssTools.minifyCssAst(self._cssAstCache);
} else if (self.css) {
var allCss = _.map(self.css, function (file) {
return file.contents('utf8');
}).join('\n');
allCss = minifiers.CleanCSSProcess(allCss);
minifiedCss = minifiers.CssTools.minifyCss(allCss);
}
self.css = [new File({ data: new Buffer(allCss, 'utf8') })];
self.css = [new File({ data: new Buffer(minifiedCss, 'utf8') })];
self.css[0].setUrlToHash(".css", "?meteor_css_resource=true");
},