diff --git a/.travis.yml b/.travis.yml index d209a1b20618..b1cd96d5c6b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,9 +5,11 @@ node_js: before_script: - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start - - npm install -g testacular@canary - - rake package - - ./nodeserver.sh > /dev/null & + - npm cache clear + - npm install -g grunt-cli + - rm -f node_modules/.bin/grunt + - grunt package + - grunt webserver > /dev/null & script: - - rake test[Firefox,"--reporters=dots"] + - grunt test --in=[Firefox] --reporters=dots diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 000000000000..4cec8829b345 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,152 @@ +var files = require('./angularFiles').files; +var util = require('./lib/grunt/utils.js'); + +module.exports = function(grunt) { + //grunt plugins + grunt.loadNpmTasks('grunt-contrib-clean'); + grunt.loadNpmTasks('grunt-contrib-copy'); + grunt.loadNpmTasks('grunt-contrib-connect'); + grunt.loadNpmTasks('grunt-contrib-compress'); + grunt.loadTasks('lib/grunt'); + + var NG_VERSION = util.getVersion(); + var dist = {}; + dist['build/angular-'+ NG_VERSION.full +'.zip'] = 'build/**'; + + //config + grunt.initConfig({ + NG_VERSION: NG_VERSION, + + connect: { + devserver: { + options: { + port: 8000, + hostname: 'localhost', + base: '.', + keepalive: true, + middleware: function(connect, options){ + return [ + //uncomment to enable CSP + // util.csp(), + util.rewrite(), + connect.favicon('images/favicon.ico'), + connect.static(options.base), + connect.directory(options.base) + ]; + } + } + }, + testserver: {} + }, + + test: { + jqlite: 'testacular-jqlite.conf.js', + jquery: 'testacular-jquery.conf.js', + modules: 'testacular-modules.conf.js', + //NOTE run grunt test:e2e instead and it will start a webserver for you + end2end: 'testacular-e2e.conf.js' + }, + + autotest: { + jqlite: 'testacular-jqlite.conf.js', + jquery: 'testacular-jquery.conf.js' + }, + + clean: {build: ['build']}, + + build: { + scenario: { + dest: 'build/angular-scenario.js', + src: [ + 'lib/jquery/jquery.js', + util.wrap([files['angularSrc'], files['angularScenario']], 'ngScenario/angular') + ], + styles: { + css: ['css/angular.css', 'css/angular-scenario.css'] + } + }, + angular: { + dest: 'build/angular.js', + src: util.wrap([files['angularSrc']], 'angular'), + styles: { + css: ['css/angular.css'], + minify: true + } + }, + loader: { + dest: 'build/angular-loader.js', + src: util.wrap(['src/loader.js'], 'loader') + }, + mocks: { + dest: 'build/angular-mocks.js', + src: ['src/ngMock/angular-mocks.js'], + strict: false + }, + sanitize: { + dest: 'build/angular-sanitize.js', + src: util.wrap([ + 'src/ngSanitize/sanitize.js', + 'src/ngSanitize/directive/ngBindHtml.js', + 'src/ngSanitize/filter/linky.js', + ], 'module') + }, + resource: { + dest: 'build/angular-resource.js', + src: util.wrap(['src/ngResource/resource.js'], 'module') + }, + cookies: { + dest: 'build/angular-cookies.js', + src: util.wrap(['src/ngCookies/cookies.js'], 'module') + }, + bootstrap: { + dest: 'build/angular-bootstrap.js', + src: util.wrap(['src/bootstrap/bootstrap.js'], 'module') + }, + bootstrapPrettify: { + dest: 'build/angular-bootstrap-prettify.js', + src: util.wrap(['src/bootstrap/bootstrap-prettify.js', 'src/bootstrap/google-prettify/prettify.js'], 'module'), + styles: { + css: ['src/bootstrap/google-prettify/prettify.css'], + minify: true + } + } + }, + + min: { + angular: 'build/angular.js', + cookies: 'build/angular-cookies.js', + loader: 'build/angular-loader.js', + resource: 'build/angular-resource.js', + sanitize: 'build/angular-sanitize.js', + bootstrap: 'build/angular-bootstrap.js', + bootstrapPrettify: 'build/angular-bootstrap-prettify.js', + }, + + docs: { + process: ['build/docs/*.html', 'build/docs/.htaccess'] + }, + + copy: { + i18n: { + files: {'build/i18n/': 'src/ngLocale/**'}, + options: {flatten: true} + } + }, + + compress: { + zip: { files: dist } + }, + + write: { + version: {file: 'build/version.txt', val: NG_VERSION.full} + } + }); + + //alias tasks + grunt.registerTask('test:unit', ['test:jqlite', 'test:jquery', 'test:modules']); + grunt.registerTask('minify', ['clean', 'build', 'minall']); + grunt.registerTask('test:e2e', ['connect:testserver', 'test:end2end']); + grunt.registerTask('webserver', ['connect:devserver']); + grunt.registerTask('package', ['clean', 'buildall', 'minall', 'docs', 'copy', 'write', 'compress']); + grunt.registerTask('default', ['package']); +}; \ No newline at end of file diff --git a/README.md b/README.md index f29dd2dd0f1e..3747c974be1e 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Building AngularJS --------- [Once you have your environment setup](http://docs.angularjs.org/misc/contribute) just run: - rake package + grunt package Running Tests @@ -32,14 +32,12 @@ Running tests requires installation of [Testacular](http://vojtajina.github.com/ To execute all unit tests, use: - rake test:unit + grunt test:unit To execute end-to-end (e2e) tests, use: - rake package - rake webserver & - rake test:e2e + grunt package + grunt test:e2e -To learn more about the rake tasks, run `rake -T` and also read our -[contribution guidelines](http://docs.angularjs.org/misc/contribute) and instructions in this -[commit message](https://github.com/angular/angular.js/commit/9d168f058f9c6d7eeae0daa7cb72ea4e02a0003a). +To learn more about the grunt tasks, run `grunt --help` and also read our +[contribution guidelines](http://docs.angularjs.org/misc/contribute). diff --git a/Rakefile b/Rakefile deleted file mode 100644 index 3a8cc00ee34a..000000000000 --- a/Rakefile +++ /dev/null @@ -1,337 +0,0 @@ -require 'yaml' -include FileUtils - - -## High level flow of the build: -## -## clean -> init -> concat -> minify -> package -## - - -content = File.open('angularFiles.js', 'r') {|f| f.read } -files = eval(content.gsub(/\};(\s|\S)*/, '}'). - gsub(/angularFiles = /, ''). - gsub(/:/, '=>'). - gsub(/\/\//, '#')); - -BUILD_DIR = 'build' - -task :default => [:package] - - -desc 'Init the build workspace' -task :init do - FileUtils.mkdir(BUILD_DIR) unless File.directory?(BUILD_DIR) - - v = YAML::load( File.open( 'version.yaml' ) ) - match = v['version'].match(/^([^-]*)(-snapshot)?$/) - - NG_VERSION = Struct.new(:full, :major, :minor, :dot, :codename, :stable). - new(match[1] + (match[2] ? ('-' + %x(git rev-parse HEAD)[0..7]) : ''), - match[1].split('.')[0], - match[1].split('.')[1], - match[1].split('.')[2].sub(/\D+.*$/, ''), - v['codename'], - v['stable']) -end - - -desc 'Clean Generated Files' -task :clean do - FileUtils.rm_r(BUILD_DIR, :force => true) - FileUtils.mkdir(BUILD_DIR) - FileUtils.rm_r('test_out', :force => true) -end - - -desc 'Concat Scenario' -task :concat_scenario => :init do - - concat_file('angular-scenario.js', [ - 'lib/jquery/jquery.js', - 'src/ngScenario/angular.prefix', - files['angularSrc'], - files['angularScenario'], - 'src/ngScenario/angular.suffix', - ], gen_css('css/angular.css') + "\n" + gen_css('css/angular-scenario.css')) -end - - - -desc 'Concat AngularJS files' -task :concat => :init do - concat_file('angular.js', [ - 'src/angular.prefix', - files['angularSrc'], - 'src/angular.suffix', - ], gen_css('css/angular.css', true)) - - FileUtils.cp_r 'src/ngLocale', path_to('i18n') - - concat_file('angular-loader.js', [ - 'src/loader.prefix', - 'src/loader.js', - 'src/loader.suffix']) - - - concat_module('sanitize', [ - 'src/ngSanitize/sanitize.js', - 'src/ngSanitize/directive/ngBindHtml.js', - 'src/ngSanitize/filter/linky.js']) - - concat_module('resource', ['src/ngResource/resource.js']) - concat_module('cookies', ['src/ngCookies/cookies.js']) - concat_module('bootstrap', ['src/bootstrap/bootstrap.js']) - concat_module('bootstrap-prettify', ['src/bootstrap/bootstrap-prettify.js', - 'src/bootstrap/google-prettify/prettify.js'], - gen_css('src/bootstrap/google-prettify/prettify.css', true)) - - - FileUtils.cp 'src/ngMock/angular-mocks.js', path_to('angular-mocks.js') - - rewrite_file(path_to('angular-mocks.js')) do |content| - content.sub!('"NG_VERSION_FULL"', NG_VERSION.full) - end -end - - -desc 'Minify JavaScript' -task :minify => [:init, :concat, :concat_scenario] do - [ 'angular.js', - 'angular-cookies.js', - 'angular-loader.js', - 'angular-resource.js', - 'angular-sanitize.js', - 'angular-bootstrap.js', - 'angular-bootstrap-prettify.js' - ].each do |file| - fork { closure_compile(file) } - end - - Process.waitall -end - - -desc 'Generate version.txt file' -task :version => [:init] do - `echo #{NG_VERSION.full} > #{path_to('version.txt')}` -end - - -desc 'Generate docs' -task :docs => [:init] do - `node docs/src/gen-docs.js` - - [ path_to('docs/.htaccess'), - path_to('docs/index.html'), - path_to('docs/index-debug.html'), - path_to('docs/index-nocache.html'), - path_to('docs/index-jq.html'), - path_to('docs/index-jq-debug.html'), - path_to('docs/index-jq-nocache.html'), - path_to('docs/docs-scenario.html') - ].each do |src| - rewrite_file(src) do |content| - content.sub!('"NG_VERSION_FULL"', NG_VERSION.full). - sub('"NG_VERSION_STABLE"', NG_VERSION.stable) - end - end -end - - -desc 'Create angular distribution' -task :package => [:clean, :minify, :version, :docs] do - zip_dir = "angular-#{NG_VERSION.full}" - zip_file = "#{zip_dir}.zip" - - FileUtils.ln_s BUILD_DIR, zip_dir - %x(zip -r #{zip_file} #{zip_dir}) - FileUtils.rm zip_dir - - FileUtils.mv zip_file, path_to(zip_file) - - puts "Package created: #{path_to(zip_file)}" -end - - -desc 'Start development webserver' -task :webserver, :port do |t, args| - exec "node lib/nodeserver/server.js #{args[:port]}" -end - - -desc 'Run all AngularJS tests' -task :test, :browsers, :misc_options do |t, args| - [ 'test:jqlite', - 'test:jquery', - 'test:modules', - 'test:e2e' - ].each do |task| - Rake::Task[task].invoke(args[:browsers], args[:misc_options]) - end -end - - -namespace :test do - - desc 'Run all unit tests (single run)' - task :unit, :browsers, :misc_options do |t, args| - [ 'test:jqlite', - 'test:jquery', - 'test:modules' - ].each do |task| - Rake::Task[task].invoke(args[:browsers], args[:misc_options]) - end - end - - - desc 'Run jqLite-based unit test suite (single run)' - task :jqlite, :browsers, :misc_options do |t, args| - start_testacular('testacular-jqlite.conf.js', true, args[:browsers], args[:misc_options]) - end - - - desc 'Run jQuery-based unit test suite (single run)' - task :jquery, :browsers, :misc_options do |t, args| - start_testacular('testacular-jquery.conf.js', true, args[:browsers], args[:misc_options]) - end - - - desc 'Run bundled modules unit test suite (single run)' - task :modules, :browsers, :misc_options do |t, args| - start_testacular('testacular-modules.conf.js', true, args[:browsers], args[:misc_options]) - end - - - desc 'Run e2e test suite (single run)' - task :e2e, :browsers, :misc_options do |t, args| - start_testacular('testacular-e2e.conf.js', true, args[:browsers], args[:misc_options]) - end -end - - -namespace :autotest do - - desc 'Run jqLite-based unit test suite (autowatch)' - task :jqlite, :browsers, :misc_options do |t, args| - start_testacular('testacular-jqlite.conf.js', false, args[:browsers], args[:misc_options]) - end - - - desc 'Run jQuery-based unit test suite (autowatch)' - task :jquery, :browsers, :misc_options do |t, args| - start_testacular('testacular-jquery.conf.js', false, args[:browsers], args[:misc_options]) - end -end - - - -################### -# utility methods # -################### - - -## -# generates css snippet from a given files and optionally applies simple minification rules -# -def gen_css(cssFile, minify = false) - css = '' - File.open(cssFile, 'r') do |f| - css = f.read - end - - if minify - css.gsub! /\n/, '' - css.gsub! /\/\*.*?\*\//, '' - css.gsub! /:\s+/, ':' - css.gsub! /\s*\{\s*/, '{' - css.gsub! /\s*\}\s*/, '}' - css.gsub! /\s*\,\s*/, ',' - css.gsub! /\s*\;\s*/, ';' - end - - #escape for js - css.gsub! /\\/, "\\\\\\" - css.gsub! /'/, "\\\\'" - css.gsub! /\n/, "\\n" - - return %Q{angular.element(document).find('head').append('');} -end - - -## -# returns path to the file in the build directory -# -def path_to(filename) - return File.join(BUILD_DIR, *filename) -end - - -def closure_compile(filename) - puts "Minifying #{filename} ..." - - min_path = path_to(filename.gsub(/\.js$/, '.min.js')) - - %x(java \ - -client \ - -d32 \ - -jar lib/closure-compiler/compiler.jar \ - --compilation_level SIMPLE_OPTIMIZATIONS \ - --language_in ECMASCRIPT5_STRICT \ - --js #{path_to(filename)} \ - --js_output_file #{min_path}) - - rewrite_file(min_path) do |content| - content.sub!("'use strict';", ""). - sub!(/\(function\([^)]*\)\{/, "\\0'use strict';") - end -end - - -def concat_file(filename, deps, footer='') - puts "Creating #{filename} ..." - File.open(path_to(filename), 'w') do |f| - concat = 'cat ' + deps.flatten.join(' ') - - content = %x{#{concat}}. - gsub('"NG_VERSION_FULL"', NG_VERSION.full). - gsub('"NG_VERSION_MAJOR"', NG_VERSION.major). - gsub('"NG_VERSION_MINOR"', NG_VERSION.minor). - gsub('"NG_VERSION_DOT"', NG_VERSION.dot). - gsub('"NG_VERSION_CODENAME"', NG_VERSION.codename). - gsub(/^\s*['"]use strict['"];?\s*$/, ''). # remove all file-specific strict mode flags - sub(/\(function\([^)]*\)\s*\{/, "\\0\n'use strict';") # add single strict mode flag - - f.write(content) - f.write(footer) - end -end - - -def concat_module(name, files, footer='') - concat_file('angular-' + name + '.js', ['src/module.prefix'] + files + ['src/module.suffix'], footer) -end - - -def rewrite_file(filename) - File.open(filename, File::RDWR) do |f| - content = f.read - - content = yield content - - raise "File rewrite failed - No content!" unless content - - f.truncate 0 - f.rewind - f.write content - end -end - - -def start_testacular(config, singleRun, browsers, misc_options) - sh "./node_modules/testacular/bin/testacular start " + - "#{config} " + - "#{'--single-run=true' if singleRun} " + - "#{'--browsers=' + browsers.gsub('+', ',') if browsers} " + - "#{(misc_options || '').gsub('+', ',')}" -end diff --git a/angularFiles.js b/angularFiles.js index 02bdf3a1d062..47f18961f736 100644 --- a/angularFiles.js +++ b/angularFiles.js @@ -79,7 +79,6 @@ angularFiles = { 'src/ngScenario/Describe.js', 'src/ngScenario/Future.js', 'src/ngScenario/ObjectModel.js', - 'src/ngScenario/Describe.js', 'src/ngScenario/Runner.js', 'src/ngScenario/SpecRunner.js', 'src/ngScenario/dsl.js', diff --git a/check-size.sh b/check-size.sh index fd46d0c9227b..1e0b7c7e4a4d 100755 --- a/check-size.sh +++ b/check-size.sh @@ -1,5 +1,5 @@ #!/bin/bash -rake minify +grunt package gzip -c < build/angular.min.js > build/angular.min.js.gzip ls -l build/angular.min.* diff --git a/docs/content/misc/contribute.ngdoc b/docs/content/misc/contribute.ngdoc index bab8ae2edde1..9e9711b08253 100644 --- a/docs/content/misc/contribute.ngdoc +++ b/docs/content/misc/contribute.ngdoc @@ -79,17 +79,13 @@ Several steps are needed to check out and build AngularJS: ## Installation Dependencies Before you can build AngularJS, you must install or configure the following dependencies on your -machine: - -* {@link http://rake.rubyforge.org Rake}: We use Rake as our build system, which is pre-installed -on most Macintosh and Linux machines. If that is not true in your case, you can grab it from the -Rake website. +machine: * Git: The {@link http://help.github.com/mac-git-installation Github Guide to Installing Git} is quite a good source for information on Git. -* {@link http://nodejs.org Node.js}: We use Node to generate the documentation and to run a -development web server. Depending on your system, you can install Node either from source or as a +* {@link http://nodejs.org Node.js}: We use Node to generate the documentation, run a +development web server, run tests, and generate a build. Depending on your system, you can install Node either from source or as a pre-packaged bundle. Once installed, you'll also need several npms (node packages), which you can install once you checked out a local copy @@ -98,6 +94,10 @@ pre-packaged bundle. * `cd angular.js` * `npm install` +* {@link http://gruntjs.com Grunt}: We use Grunt as our build system. Install the grunt command-line tool globally with: + + * `sudo npm install -g grunt-cli` + ## Creating a Github Account and Forking Angular @@ -108,7 +108,7 @@ https://github.com/angular/angular.js main angular repository}. ## Building AngularJS -To build AngularJS, you check out the source code and use Rake to generate the non-minified and +To build AngularJS, you check out the source code and use Grunt to generate the non-minified and minified AngularJS files: 1. To clone your Github repository, run: @@ -125,7 +125,7 @@ minified AngularJS files: 4. To build AngularJS, run: - rake package + grunt package The build output can be located under the `build` directory. It consists of the following files and directories: @@ -154,7 +154,7 @@ made available a local web server based on Node.js. 1. To start the web server, run: - rake webserver + grunt webserver 2. To access the local server, go to this website: @@ -169,18 +169,20 @@ made available a local web server based on Node.js. Our unit and integration tests are written with Jasmine and executed with Testacular. To run all of the tests once on Chrome run: - rake test:unit + grunt test:unit To run the tests on other browsers (Chrome, ChromeCanary, Firefox, Opera and Safari are pre-configured) use: - rake test:unit[Opera+Firefox] + grunt test:unit --in=[Opera,Firefox] + +Note there should be no spaces between browsers. [Opera, Firefox] is invalid. During development it's however more productive to continuously run unit tests every time the source or test files change. To execute tests in this mode run: 1. To start the Testacular server, capture Chrome browser and run unit tests, run: - rake autotest:jqlite + grunt autotest:jqlite 2. To capture more browsers, open this url in the desired browser (url might be different if you have multiple instance of Testacular running, read Testacular's console output for the correct url): @@ -190,9 +192,9 @@ change. To execute tests in this mode run: 3. To re-run tests just change any source or test file. -To learn more about all of the preconfigured Rake tasks run: +To learn more about all of the preconfigured Grunt tasks run: - rake -T + grunt --help ## Running the end-to-end Test Suite @@ -201,7 +203,7 @@ To run the E2E test suite: 1. Start the local web server if it's not running already. - rake webserver + grunt webserver 2. In a browser, go to: @@ -209,7 +211,13 @@ To run the E2E test suite: or in terminal run: - rake test:e2e + grunt test:end2end + +For convenience you can also simply run: + + grunt test:e2e + +This will start the webserver for you and run the tests. diff --git a/docs/src/gen-docs.js b/docs/src/gen-docs.js index 9c3ce57990f2..1661d78677f5 100755 --- a/docs/src/gen-docs.js +++ b/docs/src/gen-docs.js @@ -5,10 +5,6 @@ var reader = require('./reader.js'), appCache = require('./appCache.js').appCache, Q = require('qq'); -process.on('uncaughtException', function(err) { - console.error(err.stack || err); -}); - var start = now(); var docs; @@ -36,16 +32,16 @@ writer.makeDir('build/docs/', true).then(function() { }); }).then(function printStats() { console.log('DONE. Generated ' + docs.length + ' pages in ' + (now()-start) + 'ms.' ); -}).end(); +}).done(); function writeTheRest(writesFuture) { var metadata = ngdoc.metadata(docs); - writesFuture.push(writer.symlinkTemplate('css')); - writesFuture.push(writer.symlinkTemplate('font')); - writesFuture.push(writer.symlink('../../docs/img', 'build/docs/img')); - writesFuture.push(writer.symlinkTemplate('js')); + writesFuture.push(writer.symlinkTemplate('css', 'dir')); + writesFuture.push(writer.symlinkTemplate('font', 'dir')); + writesFuture.push(writer.symlink('../../docs/img', 'build/docs/img', 'dir')); + writesFuture.push(writer.symlinkTemplate('js', 'dir')); var manifest = 'manifest="/build/docs/appcache.manifest"'; diff --git a/docs/src/ngdoc.js b/docs/src/ngdoc.js index ae612a55e6ec..8183c40c78ab 100644 --- a/docs/src/ngdoc.js +++ b/docs/src/ngdoc.js @@ -203,7 +203,7 @@ Doc.prototype = { flush(); this.shortName = this.name.split(/[\.:#]/).pop().trim(); this.id = this.id || // if we have an id just use it - (((this.file||'').match(/.*\/([^\/]*)\.ngdoc/)||{})[1]) || // try to extract it from file name + (((this.file||'').match(/.*(\/|\\)([^(\/|\\)]*)\.ngdoc/)||{})[2]) || // try to extract it from file name this.name; // default to name this.description = this.markdown(this.description); this.example = this.markdown(this.example); diff --git a/docs/src/reader.js b/docs/src/reader.js index ded573c16ad2..a021c05ff554 100644 --- a/docs/src/reader.js +++ b/docs/src/reader.js @@ -7,7 +7,8 @@ exports.collect = collect; var ngdoc = require('./ngdoc.js'), Q = require('qq'), - qfs = require('q-fs'); + qfs = require('q-fs'), + PATH = require('path'); var NEW_LINE = /\n\r?/; @@ -43,7 +44,7 @@ function collect() { var work2; if (file.match(/\.ngdoc$/)) { work2 = Q.when(qfs.read(file, 'b'), function(content){ - var section = '@section ' + file.split('/')[2] + '\n'; + var section = '@section ' + file.split(PATH.sep)[2] + '\n'; allDocs.push(new ngdoc.Doc(section + content.toString(),file, 1).parse()); }); } diff --git a/docs/src/templates/.htaccess b/docs/src/templates/.htaccess index 98c7b7954679..e5a74cc4a4d4 100644 --- a/docs/src/templates/.htaccess +++ b/docs/src/templates/.htaccess @@ -4,7 +4,7 @@ # current angular version. If this rule matches the appcache-offline.manifest will be served for # requests to appcache.manifest # -# This file must be processed by Rake in order to replace %ANGULAR_VERSION% with the actual version. +# This file must be processed by Grunt in order to replace %ANGULAR_VERSION% with the actual version. Options -Indexes RewriteEngine on diff --git a/docs/src/writer.js b/docs/src/writer.js index 450f7cb55521..b6403e344d94 100644 --- a/docs/src/writer.js +++ b/docs/src/writer.js @@ -61,22 +61,21 @@ exports.copy = function(from, to, transform) { exports.symlink = symlink; -function symlink(from, to) { +function symlink(from, to, type) { return qfs.exists(to).then(function(exists) { if (!exists) { - return qfs.symbolicLink(to, from); + return qfs.symbolicLink(to, from, type); } }); } exports.symlinkTemplate = symlinkTemplate; -function symlinkTemplate(filename) { +function symlinkTemplate(filename, type) { var dest = OUTPUT_DIR + filename, dirDepth = dest.split('/').length, src = Array(dirDepth).join('../') + 'docs/src/templates/' + filename; - - return symlink(src, dest); + return symlink(src, dest, type); } diff --git a/lib/nodeserver/favicon.ico b/images/favicon.ico similarity index 100% rename from lib/nodeserver/favicon.ico rename to images/favicon.ico diff --git a/lib/grunt/plugins.js b/lib/grunt/plugins.js new file mode 100644 index 000000000000..4fe0b2ff75ee --- /dev/null +++ b/lib/grunt/plugins.js @@ -0,0 +1,51 @@ +var util = require('./utils.js'); +var spawn = require('child_process').spawn; + +module.exports = function(grunt) { + grunt.registerMultiTask('min', 'minify JS files', function(){ + util.min.call(util, this.data, this.async()); + }); + + grunt.registerTask('minall', 'minify all the JS files in parallel', function(){ + var files = grunt.config('min'); + files = Object.keys(files).map(function(key){ return files[key]; }); + grunt.util.async.forEach(files, util.min.bind(util), this.async()); + }); + + grunt.registerMultiTask('build', 'build JS files', function(){ + util.build.call(util, this.data, this.async()); + }); + + grunt.registerTask('buildall', 'build all the JS files in parallel', function(){ + var builds = grunt.config('build'); + builds = Object.keys(builds).map(function(key){ return builds[key]; }); + grunt.util.async.forEach(builds, util.build.bind(util), this.async()); + }); + + grunt.registerMultiTask('write', 'write content to a file', function(){ + grunt.file.write(this.data.file, this.data.val); + grunt.log.ok('wrote to ' + this.data.file); + }); + + grunt.registerMultiTask('docs', 'create angular docs', function(){ + var done = this.async(); + var files = this.data; + var docs = spawn('node', ['docs/src/gen-docs.js']); + docs.on('exit', function(code){ + if(code !== 0) grunt.fail.warn('Error creating docs'); + grunt.file.expand(files).forEach(function(file){ + grunt.file.write(file, util.process(grunt.file.read(file), grunt.config('NG_VERSION'), false)); + }); + grunt.log.ok('docs created'); + done(); + }); + }); + + grunt.registerMultiTask('test', 'Run the unit tests with testacular', function(){ + util.startTestacular.call(util, this.data, true, this.async()); + }); + + grunt.registerMultiTask('autotest', 'Run and watch the unit tests with testacular', function(){ + util.startTestacular.call(util, this.data, false, this.async()); + }); +}; \ No newline at end of file diff --git a/lib/grunt/utils.js b/lib/grunt/utils.js new file mode 100644 index 000000000000..e3d3adebd8a1 --- /dev/null +++ b/lib/grunt/utils.js @@ -0,0 +1,141 @@ +var fs = require('fs'); +var shell = require('shelljs'); +var yaml = require('yaml-js'); +var grunt = require('grunt'); +var spawn = require('child_process').spawn; + +module.exports = { + getVersion: function(){ + var version = yaml.load(fs.readFileSync('version.yaml', 'UTF-8')); + var match = version.version.match(/^([^\-]*)(-snapshot)?$/); + var semver = match[1].split('.'); + version.major = semver[0]; + version.minor = semver[1]; + version.dot = semver[2]; + var hash = shell.exec('git rev-parse --short HEAD', {silent: true}).output.replace('\n', ''); + version.full = (match[1] + (match[2] ? '-' + hash : '')); + return version; + }, + + startTestacular: function(config, singleRun, done){ + var browsers = grunt.option('in'); + var reporters = grunt.option('reporters'); + if(browsers) browsers = browsers.substr(1, browsers.length - 2); + var p = spawn('node', ['node_modules/testacular/bin/testacular', 'start', config, + singleRun ? '--single-run=true' : '', + reporters ? '--reporters=' + reporters : '', + browsers ? '--browsers=' + browsers : '' + ]); + p.stdout.pipe(process.stdout); + p.stderr.pipe(process.stderr); + p.on('exit', function(code){ + if(code !== 0) grunt.fail.warn("Test(s) failed"); + done(); + }); + }, + + wrap: function(src, name){ + src.unshift('src/' + name + '.prefix'); + src.push('src/' + name + '.suffix'); + return src; + }, + + addStyle: function(src, styles, minify){ + styles = styles.map(processCSS.bind(this)).join('\n'); + src += styles; + return src; + + function processCSS(file){ + var css = fs.readFileSync(file).toString(); + if(minify){ + css = css + .replace(/\n/g, '') + .replace(/\/\*.*?\*\//g, '') + .replace(/:\s+/g, ':') + .replace(/\s*\{\s*/g, '{') + .replace(/\s*\}\s*/g, '}') + .replace(/\s*\,\s*/g, ',') + .replace(/\s*\;\s*/g, ';'); + } + //espace for js + css = css + .replace(/\\/g, '\\\\') + .replace(/'/g, "\\'") + .replace(/\n/g, '\\n'); + return "angular.element(document).find('head').append('');"; + } + }, + + process: function(src, NG_VERSION, strict){ + var processed = src + .replace(/"NG_VERSION_FULL"/g, NG_VERSION.full) + .replace(/"NG_VERSION_MAJOR"/, NG_VERSION.major) + .replace(/"NG_VERSION_MINOR"/, NG_VERSION.minor) + .replace(/"NG_VERSION_DOT"/, NG_VERSION.dot) + .replace(/"NG_VERSION_STABLE"/, NG_VERSION.stable) + .replace(/"NG_VERSION_CODENAME"/, NG_VERSION.codename); + if (strict !== false) processed = this.singleStrict(processed, '\n\n', true); + return processed; + }, + + build: function(config, fn){ + var files = grunt.file.expand(config.src); + var styles = config.styles; + //concat + var src = files.map(function(filepath){ + return grunt.file.read(filepath); + }).join(grunt.util.normalizelf('\n')); + //process + var processed = this.process(src, grunt.config('NG_VERSION'), config.strict); + if (styles) processed = this.addStyle(processed, styles.css, styles.minify); + //write + grunt.file.write(config.dest, processed); + grunt.log.ok('File ' + config.dest + ' created.'); + fn(); + }, + + singleStrict: function(src, insert, newline){ + var useStrict = newline ? "$1\n'use strict';" : "$1'use strict';"; + return src + .replace(/\s*("|')use strict("|');\s*/g, insert) // remove all file-specific strict mode flags + .replace(/(\(function\([^)]*\)\s*\{)/, useStrict); // add single strict mode flag + }, + + min: function(file, fn) { + var minFile = file.replace(/\.js$/, '.min.js'); + var d32 = (process.platform === "win32") ? '' : ' -d32'; + shell.exec('java -client -jar' + d32 + ' lib/closure-compiler/compiler.jar' + + ' --compilation_level SIMPLE_OPTIMIZATIONS --language_in ECMASCRIPT5_STRICT ' + + '--js ' + file + ' --js_output_file ' + minFile, + function(code) { + if (code !== 0) grunt.fail.warn('Error minifying ' + file); + grunt.file.write(minFile, this.singleStrict(grunt.file.read(minFile), '\n')); + grunt.log.ok(file + ' minified into ' + minFile); + fn(); + }.bind(this)); + }, + + //csp connect middleware + csp: function(){ + return function(req, res, next){ + res.setHeader("X-WebKit-CSP", "default-src 'self';"); + res.setHeader("X-Content-Security-Policy", "default-src 'self'"); + next(); + }; + }, + + //rewrite connect middleware + rewrite: function(){ + return function(req, res, next){ + var REWRITE = /\/(guide|api|cookbook|misc|tutorial).*$/, + IGNORED = /(\.(css|js|png|jpg)$|partials\/.*\.html$)/, + match; + + if (!IGNORED.test(req.url) && (match = req.url.match(REWRITE))) { + console.log('rewriting', req.url); + req.url = req.url.replace(match[0], '/index.html'); + } + next(); + }; + } +}; \ No newline at end of file diff --git a/lib/nodeserver/server.js b/lib/nodeserver/server.js deleted file mode 100644 index cf4fc4d25b6e..000000000000 --- a/lib/nodeserver/server.js +++ /dev/null @@ -1,273 +0,0 @@ -var sys = require('sys'), - http = require('http'), - fs = require('fs'), - url = require('url'), - events = require('events'); - -var DEFAULT_PORT = 8000; - -function main(argv) { - new HttpServer({ - 'GET': createServlet(StaticServlet), - 'HEAD': createServlet(StaticServlet) - }).start(Number(argv[2]) || DEFAULT_PORT); -} - -function escapeHtml(value) { - return value.toString(). - replace('<', '<'). - replace('>', '>'). - replace('"', '"'); -} - -function createServlet(Class) { - var servlet = new Class(); - return servlet.handleRequest.bind(servlet); -} - -/** - * An Http server implementation that uses a map of methods to decide - * action routing. - * - * @param {Object} Map of method => Handler function - */ -function HttpServer(handlers) { - this.handlers = handlers; - this.server = http.createServer(this.handleRequest_.bind(this)); -} - -HttpServer.prototype.start = function(port) { - this.port = port; - this.server.listen(port); - sys.puts('Http Server running at http://127.0.0.1:' + port + '/'); -}; - -HttpServer.prototype.parseUrl_ = function(urlString) { - var parsed = url.parse(urlString); - parsed.pathname = url.resolve('/', parsed.pathname); - return url.parse(url.format(parsed), true); -}; - -HttpServer.prototype.handleRequest_ = function(req, res) { - var logEntry = req.method + ' ' + req.url; - if (req.headers['user-agent']) { - logEntry += ' ' + req.headers['user-agent']; - } - sys.puts(logEntry); - req.url = this.parseUrl_(req.url); - var handler = this.handlers[req.method]; - if (!handler) { - res.writeHead(501); - res.end(); - } else { - handler.call(this, req, res); - } -}; - -/** - * Handles static content. - */ -function StaticServlet() {} - -StaticServlet.MimeMap = { - 'txt': 'text/plain', - 'html': 'text/html', - 'css': 'text/css', - 'xml': 'application/xml', - 'json': 'application/json', - 'js': 'application/javascript', - 'jpg': 'image/jpeg', - 'jpeg': 'image/jpeg', - 'gif': 'image/gif', - 'png': 'image/png', - 'manifest': 'text/cache-manifest', - // it should be application/font-woff - // but only this silences chrome warnings - 'woff': 'font/opentype' -}; - -StaticServlet.prototype.handleRequest = function(req, res) { - var self = this; - var path = ('./' + req.url.pathname).replace('//','/').replace(/%(..)/g, function(match, hex){ - return String.fromCharCode(parseInt(hex, 16)); - }); - var parts = path.split('/'); - if (parts[parts.length-1].charAt(0) === '.') - return self.sendForbidden_(req, res, path); - - // favicon rewriting - if (path === './favicon.ico') - return self.sendFile_(req, res, './lib/nodeserver/favicon.ico'); - - // docs rewriting - var REWRITE = /\/(guide|api|cookbook|misc|tutorial).*$/, - IGNORED = /(\.(css|js|png|jpg)$|partials\/.*\.html$)/, - match; - - if (!IGNORED.test(path) && (match = path.match(REWRITE))) { - path = path.replace(match[0], '/index.html'); - sys.puts('Rewrite to ' + path); - } - - // end of docs rewriting - - fs.stat(path, function(err, stat) { - if (err) - return self.sendMissing_(req, res, path); - if (stat.isDirectory()) - return fs.stat(path + 'index.html', function(err, stat) { - // send index.html if exists - if (!err) - return self.sendFile_(req, res, path + 'index.html'); - - // list files otherwise - return self.sendDirectory_(req, res, path); - }); - - return self.sendFile_(req, res, path); - }); -}; - -StaticServlet.prototype.sendError_ = function(req, res, error) { - res.writeHead(500, { - 'Content-Type': 'text/html' - }); - res.write('\n'); - res.write('
' + escapeHtml(sys.inspect(error)) + ''); - sys.puts('500 Internal Server Error'); - sys.puts(sys.inspect(error)); -}; - -StaticServlet.prototype.sendMissing_ = function(req, res, path) { - path = path.substring(1); - res.writeHead(404, { - 'Content-Type': 'text/html' - }); - res.write('\n'); - res.write('
The requested URL ' + - escapeHtml(path) + - ' was not found on this server.
' - ); - res.end(); - sys.puts('404 Not Found: ' + path); -}; - -StaticServlet.prototype.sendForbidden_ = function(req, res, path) { - path = path.substring(1); - res.writeHead(403, { - 'Content-Type': 'text/html' - }); - res.write('\n'); - res.write('You do not have permission to access ' + - escapeHtml(path) + ' on this server.
' - ); - res.end(); - sys.puts('403 Forbidden: ' + path); -}; - -StaticServlet.prototype.sendRedirect_ = function(req, res, redirectUrl) { - res.writeHead(301, { - 'Content-Type': 'text/html', - 'Location': redirectUrl - }); - res.write('\n'); - res.write('The document has moved here.
' - ); - res.end(); - sys.puts('401 Moved Permanently: ' + redirectUrl); -}; - -StaticServlet.prototype.sendFile_ = function(req, res, path) { - var self = this; - var file = fs.createReadStream(path); - res.writeHead(200, { - // CSP headers, uncomment to enable CSP - //"X-WebKit-CSP": "default-src 'self';", - //"X-Content-Security-Policy": "default-src 'self'", - 'Content-Type': StaticServlet. - MimeMap[path.split('.').pop()] || 'text/plain' - }); - if (req.method === 'HEAD') { - res.end(); - } else { - file.on('data', res.write.bind(res)); - file.on('close', function() { - res.end(); - }); - file.on('error', function(error) { - self.sendError_(req, res, error); - }); - } -}; - -StaticServlet.prototype.sendDirectory_ = function(req, res, path) { - var self = this; - if (path.match(/[^\/]$/)) { - req.url.pathname += '/'; - var redirectUrl = url.format(url.parse(url.format(req.url))); - return self.sendRedirect_(req, res, redirectUrl); - } - fs.readdir(path, function(err, files) { - if (err) - return self.sendError_(req, res, error); - - if (!files.length) - return self.writeDirectoryIndex_(req, res, path, []); - - var remaining = files.length; - files.forEach(function(fileName, index) { - fs.stat(path + '/' + fileName, function(err, stat) { - if (err) - return self.sendError_(req, res, err); - if (stat.isDirectory()) { - files[index] = fileName + '/'; - } - if (!(--remaining)) - return self.writeDirectoryIndex_(req, res, path, files); - }); - }); - }); -}; - -StaticServlet.prototype.writeDirectoryIndex_ = function(req, res, path, files) { - path = path.substring(1); - res.writeHead(200, { - 'Content-Type': 'text/html' - }); - if (req.method === 'HEAD') { - res.end(); - return; - } - res.write('\n'); - res.write('