Browse Source

Merge pull request #67 from ckeditor/t/44

T/44 Get rid of Grunt.
Piotrek Koszuliński 10 years ago
parent
commit
c3544fd39b
45 changed files with 770 additions and 906 deletions
  1. 1 3
      CHANGES.md
  2. 0 0
      dev/tasks/build/tasks.js
  3. 0 0
      dev/tasks/build/utils.js
  4. 0 52
      dev/tasks/dev.js
  5. 61 0
      dev/tasks/dev/tasks.js
  6. 107 0
      dev/tasks/dev/tasks/create-package.js
  7. 1 1
      dev/tasks/utils/dev-init.js
  8. 9 12
      dev/tasks/utils/dev-install.js
  9. 1 1
      dev/tasks/utils/dev-relink.js
  10. 2 2
      dev/tasks/utils/dev-status.js
  11. 2 2
      dev/tasks/utils/dev-update.js
  12. 10 0
      dev/tasks/dev/templates/.gitignore
  13. 0 0
      dev/tasks/dev/templates/CHANGES.md
  14. 0 0
      dev/tasks/dev/templates/CONTRIBUTING.md
  15. 31 0
      dev/tasks/dev/templates/LICENSE.md
  16. 0 0
      dev/tasks/dev/templates/README.md
  17. 19 0
      dev/tasks/dev/templates/gulpfile.js
  18. 22 0
      dev/tasks/dev/templates/package.json
  19. 1 47
      dev/tasks/utils/git.js
  20. 10 10
      dev/tasks/utils/inquiries.js
  21. 39 130
      dev/tasks/utils/tools.js
  22. 0 13
      dev/tasks/githooks.js
  23. 0 26
      dev/tasks/jscs.js
  24. 0 26
      dev/tasks/jshint.js
  25. 92 0
      dev/tasks/lint/tasks.js
  26. 0 2
      dev/tasks/templates/LICENSE.md
  27. 0 52
      dev/tasks/utils/dev-boilerplate-update.js
  28. 0 90
      dev/tasks/utils/dev-plugin-create.js
  29. 2 2
      dev/tests/build/tasks.js
  30. 9 9
      dev/tests/build/utils.js
  31. 0 110
      dev/tests/dev-boilerplate-update.js
  32. 0 91
      dev/tests/dev-plugin-create.js
  33. 86 0
      dev/tests/dev/create-package.js
  34. 14 61
      dev/tests/git.js
  35. 2 2
      dev/tests/dev-init.js
  36. 3 10
      dev/tests/dev-install.js
  37. 2 2
      dev/tests/dev-relink.js
  38. 3 3
      dev/tests/dev-status.js
  39. 86 83
      dev/tests/tools.js
  40. 3 3
      dev/tests/dev-update.js
  41. 130 0
      dev/tests/lint.js
  42. 0 51
      gruntfile.js
  43. 12 3
      gulpfile.js
  44. 8 5
      package.json
  45. 2 2
      tests/.jshintrc

+ 1 - 3
CHANGES.md

@@ -5,6 +5,4 @@ CKEditor 5 Changelog
 
 **First Release** - Build Date: yyyy-mm-dd
 
-TODO: For the first release, we will not list new features or changes but more specifically describe CKEditor 5,
-emphasizing its differences and relation with CKEditor 4. In few words though, just like one would expect a changelog
-file to be.
+TODO: For the first release, we will not list new features or changes but more specifically describe CKEditor 5, emphasizing its differences and relation with CKEditor 4. In few words though, just like one would expect a changelog file to be.

dev/tasks/gulp/build/tasks.js → dev/tasks/build/tasks.js


dev/tasks/gulp/build/utils.js → dev/tasks/build/utils.js


+ 0 - 52
dev/tasks/dev.js

@@ -1,52 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-'use strict';
-
-const initTask = require( './utils/dev-init' );
-const pluginCreateTask = require( './utils/dev-plugin-create' );
-const pluginUpdateTask = require( './utils/dev-update' );
-const pluginStatusTask = require( './utils/dev-status' );
-const installTask = require( './utils/dev-install' );
-const relinkTask = require( './utils/dev-relink' );
-const boilerplateUpdateTask = require( './utils/dev-boilerplate-update' );
-const ckeditor5Path = process.cwd();
-
-module.exports = ( grunt ) => {
-	const packageJSON = grunt.config.data.pkg;
-	const workspaceRoot = grunt.config.data.workspaceRoot;
-
-	grunt.registerTask( 'dev-init', function() {
-		initTask( installTask, ckeditor5Path, packageJSON, workspaceRoot, grunt.log.writeln );
-	} );
-
-	grunt.registerTask( 'dev-plugin-create', function() {
-		const done = this.async();
-		pluginCreateTask( ckeditor5Path, workspaceRoot, grunt.log.writeln )
-			.then( done )
-			.catch( ( error )  => done( error ) );
-	} );
-
-	grunt.registerTask( 'dev-update', function() {
-		pluginUpdateTask( ckeditor5Path, packageJSON, workspaceRoot, grunt.log.writeln, grunt.option( 'npm-update' ) );
-	} );
-
-	grunt.registerTask( 'dev-status', function() {
-		pluginStatusTask( ckeditor5Path, packageJSON, workspaceRoot, grunt.log.writeln, grunt.log.error );
-	} );
-
-	grunt.registerTask( 'dev-boilerplate-update', function() {
-		boilerplateUpdateTask( ckeditor5Path, packageJSON, workspaceRoot, grunt.log.writeln, grunt.log.error );
-	} );
-
-	grunt.registerTask( 'dev-relink', function() {
-		relinkTask( ckeditor5Path, packageJSON, workspaceRoot, grunt.log.writeln, grunt.log.error );
-	} );
-
-	grunt.registerTask( 'dev-install', function( ) {
-		installTask( ckeditor5Path, workspaceRoot, grunt.option( 'plugin' ), grunt.log.writeln );
-	} );
-};
-

+ 61 - 0
dev/tasks/dev/tasks.js

@@ -0,0 +1,61 @@
+/* jshint node: true, esnext: true */
+
+'use strict';
+
+const gulp = require( 'gulp' );
+const minimist = require( 'minimist' );
+const statusTask = require( './tasks/status' );
+const initTask = require( './tasks/init' );
+const installTask = require( './tasks/install' );
+const pluginCreateTask = require( './tasks/create-package' );
+const updateTask = require( './tasks/update' );
+const relinkTask = require( './tasks/relink' );
+
+module.exports = ( config ) => {
+	const ckeditor5Path = process.cwd();
+	const packageJSON = require( '../../../package.json' );
+
+	gulp.task( 'init', () => {
+		initTask( installTask, ckeditor5Path, packageJSON, config.WORKSPACE_DIR, console.log );
+	} );
+
+	gulp.task( 'create-package', ( done ) => {
+		pluginCreateTask( ckeditor5Path, config.WORKSPACE_DIR, console.log )
+			.then( done )
+			.catch( ( error )  => done( error ) );
+	} );
+
+	gulp.task( 'update', () => {
+		const options = minimist( process.argv.slice( 2 ), {
+			boolean: [ 'npm-update' ],
+			default: {
+				'npm-update': false
+			}
+		} );
+
+		updateTask( ckeditor5Path, packageJSON, config.WORKSPACE_DIR, console.log, options[ 'npm-update' ] );
+	} );
+
+	gulp.task( 'status', () => {
+		statusTask( ckeditor5Path, packageJSON, config.WORKSPACE_DIR, console.log, console.error );
+	} );
+
+	gulp.task( 'relink', () => {
+		relinkTask( ckeditor5Path, packageJSON, config.WORKSPACE_DIR, console.log, console.error );
+	} );
+
+	gulp.task( 'install', () => {
+		const options = minimist( process.argv.slice( 2 ), {
+			string: [ 'package' ],
+			default: {
+				plugin: ''
+			}
+		} );
+
+		if ( options.package ) {
+			installTask( ckeditor5Path, config.WORKSPACE_DIR, options.package, console.log );
+		} else {
+			throw new Error( 'Please provide a package to install: gulp dev-install --plugin <path|GitHub URL|name>' );
+		}
+	} );
+};

+ 107 - 0
dev/tasks/dev/tasks/create-package.js

@@ -0,0 +1,107 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+const inquiries = require( '../utils/inquiries' );
+const git = require( '../utils/git' );
+const tools = require( '../utils/tools' );
+const path = require( 'path' );
+
+/**
+ * 1. Ask for new package name.
+ * 2. Ask for initial version.
+ * 3. Ask for GitHub URL.
+ * 4. Initialize repository.
+ * 5. Copy files to new repository.
+ * 6. Update package.json file in new package's repository.
+ * 7. Update package.json file in CKEditor5 repository.
+ * 8. Create initial commit.
+ * 9. Link new package.
+ * 10. Call `npm install` in package repository.
+ *
+ * @param {String} ckeditor5Path Path to main CKEditor5 repository.
+ * @param {String} workspaceRoot Relative path to workspace root.
+ * @param {Function} writeln Function for log output.
+ * @returns {Promise} Returns promise fulfilled after task is done.
+ */
+module.exports = ( ckeditor5Path, workspaceRoot, writeln ) => {
+	const workspaceAbsolutePath = path.join( ckeditor5Path, workspaceRoot );
+	const fileStructure = {
+		'./': [
+			'.editorconfig',
+			'.jshintrc',
+			'.jscsrc',
+			'.gitattributes',
+			'./dev/tasks/dev/templates'
+		],
+		'tests/': [
+			'tests/.jshintrc'
+		],
+		'dev/': [
+			'dev/.jshintrc'
+		],
+		'dev/tasks/lint': [
+			'dev/tasks/lint'
+		]
+	};
+
+	let packageName;
+	let repositoryPath;
+	let packageVersion;
+	let gitHubUrl;
+
+	return inquiries.getPackageName()
+		.then( result => {
+			packageName = result;
+			repositoryPath = path.join( workspaceAbsolutePath, packageName );
+
+			return inquiries.getPackageVersion();
+		} )
+		.then( result => {
+			packageVersion = result;
+
+			return inquiries.getPackageGitHubUrl( packageName );
+		} )
+		.then( result => {
+			gitHubUrl = result;
+
+			writeln( `Initializing repository ${ repositoryPath }...` );
+			git.initializeRepository( repositoryPath );
+
+			writeln( `Copying files into ${ repositoryPath }...` );
+
+			for ( let destination in fileStructure ) {
+				tools.copy( fileStructure[ destination ], path.join( repositoryPath, destination ) );
+			}
+
+			writeln( `Updating package.json files...` );
+			tools.updateJSONFile( path.join( repositoryPath, 'package.json' ), ( json ) => {
+				json.name = packageName;
+				json.version = packageVersion;
+
+				return json;
+			} );
+
+			tools.updateJSONFile( path.join( ckeditor5Path, 'package.json' ), ( json ) => {
+				if ( !json.dependencies ) {
+					json.dependencies = {};
+				}
+				json.dependencies[ packageName ] = gitHubUrl;
+				json.dependencies = tools.sortObject( json.dependencies );
+
+				return json;
+			} );
+
+			writeln( `Creating initial commit...` );
+			git.initialCommit( packageName, repositoryPath );
+
+			writeln( `Linking ${ packageName } to node_modules...` );
+			tools.linkDirectories( repositoryPath, path.join( ckeditor5Path, 'node_modules', packageName ) );
+
+			writeln( `Running npm install in ${ packageName }.` );
+			tools.npmInstall( repositoryPath );
+		} );
+};

+ 1 - 1
dev/tasks/utils/dev-init.js

@@ -5,7 +5,7 @@
 
 'use strict';
 
-const tools = require( './tools' );
+const tools = require( '../utils/tools' );
 
 /**
  * 1. Get CKEditor5 dependencies from package.json file.

+ 9 - 12
dev/tasks/utils/dev-install.js

@@ -5,28 +5,27 @@
 
 'use strict';
 
-const git = require( './git' );
-const tools = require( './tools' );
+const git = require( '../utils/git' );
+const tools = require( '../utils/tools' );
 const path = require( 'path' );
 
 /**
- * This tasks install specified module in development mode. It can be executed by typing:
- * 		grunt dev-install --plugin <git_hub_url|npm_name|path_on_disk>
+ * This tasks install specified package in development mode. It can be executed by typing:
+ * 		grunt dev-install --package <git_hub_url|npm_name|path_on_disk>
  *
  *
  * It performs following steps:
  * 1. If GitHub URL is provided - clones the repository.
  * 2. If NPM module name is provided - gets GitHub URL from NPM and clones the repository.
  * 3. If path on disk is provided - it is used directly.
- * 4. Runs `npm install` in plugin repository.
- * 5. If plugin exists in `ckeditor5/node_modules/` - runs `npm uninstall plugin_name`.
- * 6. Links plugin directory into `ckeditor5/node_modules/`.
+ * 4. Runs `npm install` in package repository.
+ * 5. If package exists in `ckeditor5/node_modules/` - runs `npm uninstall package_name`.
+ * 6. Links package directory into `ckeditor5/node_modules/`.
  * 7. Adds dependency to `ckeditor5/package.json`.
- * 8. Installs Git hooks.
  *
  * @param {String} ckeditor5Path Absolute path to `ckeditor5` repository.
  * @param {String} workspaceRoot Relative path to workspace root directory.
- * @param {String} name Name of the NPM module or GitHub URL.
+ * @param {String} name Name of the NPM package or GitHub URL.
  * @param {Function} writeln Function used to report progress to the console.
  */
 module.exports = ( ckeditor5Path, workspaceRoot, name, writeln ) => {
@@ -102,12 +101,10 @@ module.exports = ( ckeditor5Path, workspaceRoot, name, writeln ) => {
 		tools.updateJSONFile( path.join( ckeditor5Path, 'package.json' ), ( json ) => {
 			json.dependencies = json.dependencies || {};
 			json.dependencies[ urlInfo.name ] = dependency;
+			json.dependencies = tools.sortObject( json.dependencies );
 
 			return json;
 		} );
-
-		writeln( `Installing Git hooks in ${ urlInfo.name }...` );
-		tools.installGitHooks( repositoryPath );
 	} else {
 		throw new Error( 'Please provide valid GitHub URL, NPM module name or path.' );
 	}

+ 1 - 1
dev/tasks/utils/dev-relink.js

@@ -5,7 +5,7 @@
 
 'use strict';
 
-const tools = require( './tools' );
+const tools = require( '../utils/tools' );
 const path = require( 'path' );
 
 /**

+ 2 - 2
dev/tasks/utils/dev-status.js

@@ -5,8 +5,8 @@
 
 'use strict';
 
-const tools = require( './tools' );
-const git = require( './git' );
+const tools = require( '../utils/tools' );
+const git = require( '../utils/git' );
 const path = require( 'path' );
 
 /**

+ 2 - 2
dev/tasks/utils/dev-update.js

@@ -5,8 +5,8 @@
 
 'use strict';
 
-const tools = require( './tools' );
-const git = require( './git' );
+const tools = require( '../utils/tools' );
+const git = require( '../utils/git' );
 const path = require( 'path' );
 
 /**

+ 10 - 0
dev/tasks/dev/templates/.gitignore

@@ -0,0 +1,10 @@
+# These files will be ignored by Git and by our linting tools:
+#	grunt jshint
+#	grunt jscs
+#
+# Be sure to append /** to folders to have everything inside them ignored.
+
+# All "dot directories".
+.*/**
+
+node_modules/**

dev/tasks/templates/CHANGES.md → dev/tasks/dev/templates/CHANGES.md


dev/tasks/templates/CONTRIBUTING.md → dev/tasks/dev/templates/CONTRIBUTING.md


+ 31 - 0
dev/tasks/dev/templates/LICENSE.md

@@ -0,0 +1,31 @@
+Software License Agreement
+==========================
+
+**CKEditor 5** - https://github.com/ckeditor/ckeditor5 <br>
+Copyright (c) 2003-2014, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.
+
+Licensed under the terms of any of the following licenses at your choice:
+
+ * [GNU General Public License Version 2 or later (the "GPL")](http://www.gnu.org/licenses/gpl.html)
+
+ * [GNU Lesser General Public License Version 2.1 or later (the "LGPL")](http://www.gnu.org/licenses/lgpl.html)
+
+ * [Mozilla Public License Version 1.1 or later (the "MPL")](http://www.mozilla.org/MPL/MPL-1.1.html)
+
+You are not required to, but if you want to explicitly declare the license you have chosen to be bound to when using,
+reproducing, modifying and distributing this software, just include a text file titled "legal.txt" in your version of
+this software, indicating your license choice. In any case, your choice will not restrict any recipient of your version
+of this software to use, reproduce, modify and distribute this software under any of the above licenses.
+
+Sources of Intellectual Property Included in CKEditor
+-----------------------------------------------------
+
+Where not otherwise indicated, all CKEditor content is authored by CKSource engineers and consists of CKSource-owned
+intellectual property. In some specific instances, CKEditor will incorporate work done by developers outside of CKSource
+with their express permission.
+
+Trademarks
+----------
+
+**CKEditor** is a trademark of [CKSource](http://cksource.com) Frederico Knabben. All other brand and product names are
+trademarks, registered trademarks or service marks of their respective holders.

dev/tasks/templates/README.md → dev/tasks/dev/templates/README.md


+ 19 - 0
dev/tasks/dev/templates/gulpfile.js

@@ -0,0 +1,19 @@
+/* jshint node: true */
+
+'use strict';
+
+const gulp = require( 'gulp' );
+
+const config = {
+	ROOT_DIR: '.',
+	WORKSPACE_DIR: '..',
+
+	// Files ignored by jshint and jscs tasks. Files from .gitignore will be added automatically during tasks execution.
+	IGNORED_FILES: [
+		'src/lib/**'
+	]
+};
+
+require( './dev/tasks/lint/tasks' )( config );
+
+gulp.task( 'pre-commit', [ 'lint-staged' ] );

+ 22 - 0
dev/tasks/dev/templates/package.json

@@ -0,0 +1,22 @@
+{
+	"name": "",
+	"version": "",
+	"description": "",
+	"keywords": [],
+	"dependencies": {},
+	"devDependencies": {
+		"git-guppy": "^1.0.1",
+		"gulp": "^3.9.0",
+		"gulp-filter": "^3.0.1",
+		"gulp-jscs": "^3.0.2",
+		"gulp-jshint": "^2.0.0",
+		"guppy-pre-commit": "^0.3.0",
+		"jshint": "^2.9.1",
+		"jshint-reporter-jscs": "^0.1.0"
+	},
+	"author": "CKSource (http://cksource.com/)",
+	"license": "See LICENSE.md",
+	"homepage": "",
+	"bugs": "",
+	"repository": ""
+}

+ 1 - 47
dev/tasks/utils/git.js

@@ -8,23 +8,6 @@
 const tools = require( './tools' );
 
 module.exports = {
-	/**
-	 * Holds boilerplate repository Git URL.
-	 *
-	 * @private
-	 * @readonly
-	 * @type {String}
-	 */
-	BOILERPLATE_REPOSITORY: 'git@github.com:ckeditor/ckeditor-boilerplate.git',
-
-	/**
-	 * Holds boilerplate branch used in CKEditor5 projects.
-	 *
-	 * @private
-	 * @readonly
-	 * @type {String}
-	 */
-	BOILERPLATE_BRANCH: 'ckeditor5',
 
 	/**
 	 * Parses GitHub URL. Extracts used server, repository and branch.
@@ -118,15 +101,7 @@ module.exports = {
 	 * @param {String} repositoryPath Absolute path where repository should be created.
 	 */
 	initializeRepository( repositoryPath ) {
-		const initializeCommands = [
-			`git init ${ repositoryPath }`,
-			`cd ${ repositoryPath }`,
-			`git remote add boilerplate ${ this.BOILERPLATE_REPOSITORY }`,
-			`git fetch boilerplate ${ this.BOILERPLATE_BRANCH }`,
-			`git merge boilerplate/${ this.BOILERPLATE_BRANCH }`
-		];
-
-		tools.shExec( initializeCommands.join( ' && ' ) );
+		tools.shExec( `git init ${ repositoryPath }` );
 	},
 
 	/**
@@ -140,27 +115,6 @@ module.exports = {
 	},
 
 	/**
-	 * Updates boilerplate project in specified repository.
-	 * @param {String} repositoryPath Absolute path to repository.
-	 */
-	updateBoilerplate( repositoryPath ) {
-		const regexp = /boilerplate(\n|$)/;
-
-		// Try to add boilerplate remote if one is not already added.
-		if ( !regexp.test( tools.shExec( `cd ${ repositoryPath } && git remote` ) ) ) {
-			tools.shExec( `cd ${ repositoryPath } && git remote add boilerplate ${ this.BOILERPLATE_REPOSITORY }` );
-		}
-
-		const updateCommands = [
-			`cd ${ repositoryPath }`,
-			`git fetch boilerplate ${ this.BOILERPLATE_BRANCH }`,
-			`git merge boilerplate/${ this.BOILERPLATE_BRANCH }`
-		];
-
-		tools.shExec( updateCommands.join( ' && ' ) );
-	},
-
-	/**
 	 * Creates initial commit on repository under specified path.
 	 *
 	 * @param {String} pluginName

+ 10 - 10
dev/tasks/utils/inquiries.js

@@ -11,27 +11,27 @@ const DEFAULT_PLUGIN_VERSION = '0.0.1';
 const DEFAULT_GITHUB_URL_PREFIX = 'ckeditor/';
 
 module.exports = {
-	getPluginName() {
+	getPackageName() {
 		return new Promise( ( resolve ) => {
 			inquirer.prompt( [ {
-				name: 'pluginName',
-				message: 'Enter plugin name without ' + DEFAULT_PLUGIN_NAME_PREFIX + ' prefix:',
+				name: 'packageName',
+				message: 'Enter package name without ' + DEFAULT_PLUGIN_NAME_PREFIX + ' prefix:',
 				validate: ( input ) => {
 					const regexp = /^[\w-]+$/;
 
-					return regexp.test( input ) ? true : 'Please provide a valid plugin name.';
+					return regexp.test( input ) ? true : 'Please provide a valid package name.';
 				}
 			} ], ( answers ) => {
-				resolve( DEFAULT_PLUGIN_NAME_PREFIX + answers.pluginName );
+				resolve( DEFAULT_PLUGIN_NAME_PREFIX + answers.packageName );
 			} );
 		} );
 	},
 
-	getPluginVersion( ) {
+	getPackageVersion( ) {
 		return new Promise( ( resolve ) => {
 			inquirer.prompt( [ {
 				name: 'version',
-				message: 'Enter plugin\'s initial version:',
+				message: 'Enter package\'s initial version:',
 				default: DEFAULT_PLUGIN_VERSION
 			} ], ( answers ) => {
 				resolve( answers.version );
@@ -39,13 +39,13 @@ module.exports = {
 		} );
 	},
 
-	getPluginGitHubUrl( pluginName ) {
-		const defaultGitHubUrl = DEFAULT_GITHUB_URL_PREFIX + pluginName;
+	getPackageGitHubUrl( packageName ) {
+		const defaultGitHubUrl = DEFAULT_GITHUB_URL_PREFIX + packageName;
 
 		return new Promise( ( resolve ) => {
 			inquirer.prompt( [ {
 				name: 'gitHubUrl',
-				message: 'Enter plugin\'s GitHub URL:',
+				message: 'Enter package\'s GitHub URL:',
 				default: defaultGitHubUrl
 			} ], ( answers ) => {
 				resolve( answers.gitHubUrl );

+ 39 - 130
dev/tasks/utils/tools.js

@@ -1,123 +1,8 @@
 'use strict';
 
-let dirtyFiles,
-	ignoreList;
-
 const dependencyRegExp = /^ckeditor5-/;
-const TEMPLATE_PATH = './dev/tasks/templates';
 
 module.exports = {
-	/**
-	 * Check if a task (including its optional target) is in the queue of tasks to be executed by Grunt.
-	 *
-	 * @param grunt {Object} The Grunt object.
-	 * @param task {String} The task name. May optionally include the target (e.g. 'task:target').
-	 * @returns {Boolean} "true" if the task is in the queue.
-	 */
-	checkTaskInQueue( grunt, task ) {
-		const cliTasks = grunt.cli.tasks;
-
-		// Check if the task has been called directly.
-		const isDirectCall = ( cliTasks.indexOf( task ) > -1 );
-		// Check if this is a "default" call and that the task is inside "default".
-		const isDefaultTask = ( cliTasks.indexOf( 'default' ) > -1 ) || !cliTasks.length;
-		// Hacking Grunt hard.
-		const isTaskInDefault = isDefaultTask && ( grunt.task._tasks.default.info.indexOf( '"' + task + '"' ) > -1 );
-
-		return isDirectCall || isTaskInDefault;
-	},
-
-	/**
-	 * Configures a multi-task and defines targets that are queued to be run by Grunt.
-	 *
-	 * @param grunt {Object} The Grunt object.
-	 * @param options {Object} A list of options for the method. See the jscs and jshint tasks for example.
-	 */
-	setupMultitaskConfig( grunt, options ) {
-		const task = options.task;
-		const taskConfig = {};
-		const config = taskConfig[ task ] = {
-			options: options.defaultOptions
-		};
-
-		// "all" is the default target to be used if others are not to be run.
-		const all = options.targets.all;
-		let isAll = true;
-
-		delete options.targets.all;
-
-		Object.getOwnPropertyNames( options.targets ).forEach( function( target ) {
-			if ( this.checkTaskInQueue( grunt, task + ':' + target ) ) {
-				config[ target ] = options.targets[ target ]();
-				isAll = false;
-			}
-		}, this );
-
-		if ( isAll ) {
-			config.all = all();
-		}
-
-		// Append .gitignore entries to the ignore list.
-		if ( options.addGitIgnore ) {
-			let ignoreProp = task + '.options.' + options.addGitIgnore;
-			let ignores = grunt.config.get( ignoreProp ) || [];
-
-			ignores = ignores.concat( this.getGitIgnore( grunt ) );
-			grunt.config.set( ignoreProp, ignores );
-		}
-
-		// Merge over configurations set in gruntfile.js.
-		grunt.config.merge( taskConfig );
-	},
-
-	/**
-	 * Gets the list of ignores from `.gitignore`.
-	 *
-	 * @param grunt {Object} The Grunt object.
-	 * @returns {String[]} The list of ignores.
-	 */
-	getGitIgnore( grunt ) {
-		if ( !ignoreList ) {
-			ignoreList = grunt.file.read( '.gitignore' );
-
-			ignoreList = ignoreList
-				// Remove comment lines.
-				.replace( /^#.*$/gm, '' )
-				// Transform into array.
-				.split( /\n+/ )
-				// Remove empty entries.
-				.filter( function( path ) {
-					return !!path;
-				} );
-		}
-
-		return ignoreList;
-	},
-
-	/**
-	 * Gets the list of files that are supposed to be included in the next Git commit.
-	 *
-	 * @returns {String[]} A list of file paths.
-	 */
-	getGitDirtyFiles() {
-		// Cache it, so it is executed only once when running multiple tasks.
-		if ( !dirtyFiles ) {
-			dirtyFiles = this
-				// Compare the state of index with HEAD.
-				.shExec( 'git diff-index --name-only HEAD' )
-				// Remove trailing /n to avoid an empty entry.
-				.replace( /\s*$/, '' )
-				// Transform into array.
-				.split( '\n' );
-
-			// If nothing is returned, the array will one one empty string only.
-			if ( dirtyFiles.length == 1 && !dirtyFiles[ 0 ] ) {
-				dirtyFiles = [];
-			}
-		}
-
-		return dirtyFiles;
-	},
 
 	/**
 	 * Executes a shell command.
@@ -253,7 +138,24 @@ module.exports = {
 		let json = JSON.parse( contents );
 		json = updateFunction( json );
 
-		fs.writeFileSync( path, JSON.stringify( json, null, 2 ), 'utf-8' );
+		fs.writeFileSync( path, JSON.stringify( json, null, 2 ) + '\n', 'utf-8' );
+	},
+
+	/**
+	 * Reinserts all object's properties in alphabetical order (character's Unicode value).
+	 * Used for JSON.stringify method which takes keys in insertion order.
+	 *
+	 * @param { Object } obj
+	 * @returns { Object } Same object with sorted keys.
+	 */
+	sortObject( obj ) {
+		Object.keys( obj ).sort().forEach( key => {
+			const val = obj[ key ];
+			delete obj[ key ];
+			obj[ key ] = val;
+		} );
+
+		return obj;
 	},
 
 	/**
@@ -304,23 +206,30 @@ module.exports = {
 	},
 
 	/**
-	 * Installs Git hooks in specified repository.
+	 * Copies source files/directories into destination directory.
+	 * If directory path is provided in sources array - all files inside that directory will be copied.
 	 *
-	 * @param {String} path
+	 * @param { Array } source Source files/directories.
+	 * @param { String} destination Path to destination directory.
 	 */
-	installGitHooks( path ) {
-		this.shExec( `cd ${ path } && grunt githooks` );
-	},
-
-	/**
-	 * Copies template files to specified destination.
-	 *
-	 * @param {String} destination
-	 */
-	copyTemplateFiles( destination ) {
+	copy( sources, destination ) {
 		const path = require( 'path' );
-		const templatesPath = path.resolve( TEMPLATE_PATH );
-		this.shExec( `cp ${ path.join( templatesPath, '*.md' ) } ${ destination }` );
+		const fs = require( 'fs-extra' );
+		destination = path.resolve( destination );
+
+		fs.ensureDirSync( destination );
+
+		sources.forEach( source => {
+			source = path.resolve( source );
+
+			if ( this.isFile( source ) ) {
+				fs.copySync( source, path.join( destination, path.basename( source ) ) );
+			}
+
+			if ( this.isDirectory( source ) ) {
+				fs.copySync( source, destination );
+			}
+		} );
 	},
 
 	/**

+ 0 - 13
dev/tasks/githooks.js

@@ -1,13 +0,0 @@
-'use strict';
-
-module.exports = function( grunt ) {
-	grunt.config.merge( {
-		githooks: {
-			all: {
-				'pre-commit': 'default'
-			}
-		}
-	} );
-
-	grunt.loadNpmTasks( 'grunt-githooks' );
-};

+ 0 - 26
dev/tasks/jscs.js

@@ -1,26 +0,0 @@
-'use strict';
-
-const tools = require( './utils/tools' );
-
-module.exports = ( grunt ) => {
-	tools.setupMultitaskConfig( grunt, {
-		task: 'jscs',
-		defaultOptions: {
-				config: true
-			},
-		addGitIgnore: 'excludeFiles',
-		targets: {
-			all() {
-				return [ '**/*.js' ];
-			},
-
-			git() {
-				return tools.getGitDirtyFiles().filter( function( file ) {
-					return ( /\.js$/ ).test( file );
-				} );
-			}
-		}
-	} );
-
-	grunt.loadNpmTasks( 'grunt-jscs' );
-};

+ 0 - 26
dev/tasks/jshint.js

@@ -1,26 +0,0 @@
-'use strict';
-
-const tools = require( './utils/tools' );
-
-module.exports = ( grunt ) => {
-	tools.setupMultitaskConfig( grunt, {
-		task: 'jshint',
-		defaultOptions: {
-				jshintrc: true
-			},
-		addGitIgnore: 'ignores',
-		targets: {
-			all() {
-				return [ '**/*.js' ];
-			},
-
-			git() {
-				return tools.getGitDirtyFiles().filter( function( file ) {
-					return ( /\.js$/ ).test( file );
-				} );
-			}
-		}
-	} );
-
-	grunt.loadNpmTasks( 'grunt-contrib-jshint' );
-};

+ 92 - 0
dev/tasks/lint/tasks.js

@@ -0,0 +1,92 @@
+/* jshint node: true, esnext: true */
+
+'use strict';
+
+const gulp = require( 'gulp' );
+const jshint = require( 'gulp-jshint' );
+const jscs = require( 'gulp-jscs' );
+const fs = require( 'fs' );
+const guppy = require( 'git-guppy' )( gulp );
+const gulpFilter = require( 'gulp-filter' );
+const gutil = require( 'gulp-util' );
+
+module.exports = ( config ) => {
+	const src = [ '**/*.js' ].concat( config.IGNORED_FILES.map( i => '!' + i ), getGitIgnore() );
+
+	const tasks = {
+		/**
+		 * Returns stream containing jshint and jscs reporters.
+		 *
+		 * @returns {Stream}
+		 */
+		lint() {
+			return gulp.src( src )
+				.pipe( lint() );
+		},
+
+		/**
+		 * This method is executed on pre-commit hook, linting only files staged for current commit.
+		 *
+		 * @returns {Stream}
+		 */
+		lintStaged() {
+			return guppy.stream( 'pre-commit' )
+				.pipe( gulpFilter( src ) )
+				.pipe( lint() )
+
+				// Error reporting for gulp.
+				.pipe( jscs.reporter( 'fail' ) )
+				.on( 'error', errorHandler )
+				.pipe( jshint.reporter( 'fail' ) )
+				.on( 'error', errorHandler );
+
+			/**
+			 * Handles error from jscs and jshint fail reporters. Stops node process with error code
+			 * and prints error message to the console.
+			 */
+			function errorHandler() {
+				gutil.log( gutil.colors.red( 'Linting failed, commit aborted' ) );
+				process.exit( 1 );
+			}
+		}
+	};
+
+	gulp.task( 'lint', tasks.lint );
+	gulp.task( 'lint-staged', tasks.lintStaged );
+
+	return tasks;
+
+	/**
+	 * Gets the list of ignores from `.gitignore`.
+	 *
+	 * @returns {String[]} The list of ignores.
+	 */
+	function getGitIgnore( ) {
+		let gitIgnoredFiles = fs.readFileSync( '.gitignore', 'utf8' );
+
+		return gitIgnoredFiles
+			// Remove comment lines.
+			.replace( /^#.*$/gm, '' )
+			// Transform into array.
+			.split( /\n+/ )
+			// Remove empty entries.
+			.filter( ( path ) => !!path )
+			// Add `!` for ignore glob.
+			.map( i => '!' + i );
+	}
+
+	/**
+	 * Returns stream with all linting plugins combined.
+	 *
+	 * @returns {Stream}
+	 */
+	function lint() {
+		const stream = jshint();
+		stream
+			.pipe( jscs() )
+			.pipe( jscs.reporter() )
+			.pipe( jshint.reporter( 'jshint-reporter-jscs' ) );
+
+		return stream;
+	}
+};

+ 0 - 2
dev/tasks/templates/LICENSE.md

@@ -1,2 +0,0 @@
-Software License Agreement
-==========================

+ 0 - 52
dev/tasks/utils/dev-boilerplate-update.js

@@ -1,52 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-'use strict';
-
-const tools = require( './tools' );
-const git = require( './git' );
-const path = require( 'path' );
-
-/**
- * 1. Get CKEditor5 dependencies from package.json file.
- * 2. Scan workspace for repositories that match dependencies from package.json file.
- * 3. Fetch and merge boilerplate remote.
- *
- * @param {String} ckeditor5Path Path to main CKEditor5 repository.
- * @param {Object} packageJSON Parsed package.json file from CKEditor5 repository.
- * @param {String} workspaceRoot Relative path to workspace root.
- * @param {Function} writeln Function for log output.
- * @param {Function} writeError Function of error output
- */
-module.exports = ( ckeditor5Path, packageJSON, workspaceRoot, writeln, writeError ) => {
-	const workspaceAbsolutePath = path.join( ckeditor5Path, workspaceRoot );
-
-	// Get all CKEditor dependencies from package.json.
-	const dependencies = tools.getCKEditorDependencies( packageJSON.dependencies );
-
-	if ( dependencies ) {
-		const directories = tools.getCKE5Directories( workspaceAbsolutePath );
-
-		if ( directories.length ) {
-			for ( let dependency in dependencies ) {
-				const repositoryAbsolutePath = path.join( workspaceAbsolutePath, dependency );
-
-				// Check if repository's directory already exists.
-				if ( directories.indexOf( dependency ) > -1 ) {
-					try {
-						writeln( `Updating boilerplate in ${ dependency }...` );
-						git.updateBoilerplate( repositoryAbsolutePath );
-					} catch ( error ) {
-						writeError( error );
-					}
-				}
-			}
-		} else {
-			writeln( 'No CKEditor5 plugins in development mode.' );
-		}
-	} else {
-		writeln( 'No CKEditor5 dependencies found in package.json file.' );
-	}
-};

+ 0 - 90
dev/tasks/utils/dev-plugin-create.js

@@ -1,90 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-'use strict';
-
-const inquiries = require( './inquiries' );
-const git = require( './git' );
-const tools = require( './tools' );
-const path = require( 'path' );
-
-/**
- * 1. Ask for new plugin name.
- * 2. Ask for initial version.
- * 3. Ask for GitHub URL.
- * 4. Initialize repository
- * 		4.1. Initialize Git repository.
- * 		4.2. Fetch and merge boilerplate project.
- * 5. Copy template files.
- * 6. Update package.json file in new plugin's repository.
- * 7. Update package.json file in CKEditor5 repository.
- * 8. Create initial commit.
- * 9. Link new plugin.
- * 10. Call `npm install` in plugin repository.
- * 11. Install Git hooks in plugin repository.
- *
- * @param {String} ckeditor5Path Path to main CKEditor5 repository.
- * @param {String} workspaceRoot Relative path to workspace root.
- * @param {Function} writeln Function for log output.
- * @returns {Promise} Returns promise fulfilled after task is done.
- */
-module.exports = ( ckeditor5Path, workspaceRoot, writeln ) => {
-	const workspaceAbsolutePath = path.join( ckeditor5Path, workspaceRoot );
-	let pluginName;
-	let repositoryPath;
-	let pluginVersion;
-	let gitHubUrl;
-
-	return inquiries.getPluginName()
-		.then( result => {
-			pluginName = result;
-			repositoryPath = path.join( workspaceAbsolutePath, pluginName );
-
-			return inquiries.getPluginVersion();
-		} )
-		.then( result => {
-			pluginVersion = result;
-
-			return inquiries.getPluginGitHubUrl( pluginName );
-		} )
-		.then( result => {
-			gitHubUrl = result;
-
-			writeln( `Initializing repository ${ repositoryPath }...` );
-			git.initializeRepository( repositoryPath );
-
-			writeln( `Copying template files to ${ repositoryPath }...` );
-			tools.copyTemplateFiles( repositoryPath );
-
-			writeln( `Updating package.json files...` );
-			tools.updateJSONFile( path.join( repositoryPath, 'package.json' ), ( json ) => {
-				json.name = pluginName;
-				json.version = pluginVersion;
-
-				return json;
-			} );
-
-			tools.updateJSONFile( path.join( ckeditor5Path, 'package.json' ), ( json ) => {
-				if ( !json.dependencies ) {
-					json.dependencies = {};
-				}
-				json.dependencies[ pluginName ] = gitHubUrl;
-
-				return json;
-			} );
-
-			writeln( `Creating initial commit...` );
-			git.initialCommit( pluginName, repositoryPath );
-
-			writeln( `Linking ${ pluginName } to node_modules...` );
-			tools.linkDirectories( repositoryPath, path.join( ckeditor5Path, 'node_modules', pluginName ) );
-
-			writeln( `Running npm install in ${ pluginName }.` );
-			tools.npmInstall( repositoryPath );
-
-			writeln( `Installing Git hooks in ${ pluginName }.` );
-			tools.installGitHooks( repositoryPath );
-		} );
-};

+ 2 - 2
dev/tests/build/tasks.js

@@ -10,7 +10,7 @@
 const mockery = require( 'mockery' );
 const sinon = require( 'sinon' );
 const Vinyl = require( 'vinyl' );
-const utils = require( '../../tasks/gulp/build/utils' );
+const utils = require( '../../tasks/build/utils' );
 const babel = require( 'babel-core' );
 const chai = require( 'chai' );
 const expect = chai.expect;
@@ -41,7 +41,7 @@ describe( 'build-tasks', () => {
 			};
 		} );
 
-		tasks = require( '../../tasks/gulp/build/tasks' )( config );
+		tasks = require( '../../tasks/build/tasks' )( config );
 	} );
 
 	afterEach( () => {

+ 9 - 9
dev/tests/build/utils.js

@@ -18,7 +18,7 @@ const Vinyl = require( 'vinyl' );
 const through = require( 'through2' );
 
 describe( 'build-utils', () => {
-	const utils = require( '../../tasks/gulp/build/utils' );
+	const utils = require( '../../tasks/build/utils' );
 	let sandbox;
 
 	beforeEach( () => {
@@ -290,14 +290,14 @@ describe( 'build-utils', () => {
 
 			rename.pipe(
 				utils.noop( ( data ) => {
-					expect( data.path ).to.equal( 'ckeditor5/core/foo/file.js' );
+					expect( data.path ).to.equal( path.normalize( 'ckeditor5/core/foo/file.js' ) );
 					done();
 				} )
 			);
 
 			rename.write( new Vinyl( {
 				cwd: './',
-				path: 'ckeditor5-core/src/foo/file.js',
+				path: path.normalize( 'ckeditor5-core/src/foo/file.js' ),
 				contents: new Buffer( '' )
 			} ) );
 
@@ -309,14 +309,14 @@ describe( 'build-utils', () => {
 
 			rename.pipe(
 				utils.noop( ( data ) => {
-					expect( data.path ).to.equal( 'tests/core/foo/file.js' );
+					expect( data.path ).to.equal( path.normalize( 'tests/core/foo/file.js' ) );
 					done();
 				} )
 			);
 
 			rename.write( new Vinyl( {
 				cwd: './',
-				path: 'ckeditor5-core/tests/foo/file.js',
+				path: path.normalize( 'ckeditor5-core/tests/foo/file.js' ),
 				contents: new Buffer( '' )
 			} ) );
 
@@ -354,14 +354,14 @@ describe( 'build-utils', () => {
 
 			rename.pipe(
 				utils.noop( ( data ) => {
-					expect( data.path ).to.equal( 'ckeditor5/foo/file.js' );
+					expect( data.path ).to.equal( path.normalize( 'ckeditor5/foo/file.js' ) );
 					done();
 				} )
 			);
 
 			rename.write( new Vinyl( {
 				cwd: './',
-				path: 'src/foo/file.js',
+				path: path.normalize( 'src/foo/file.js' ),
 				contents: new Buffer( '' )
 			} ) );
 
@@ -373,14 +373,14 @@ describe( 'build-utils', () => {
 
 			rename.pipe(
 				utils.noop( ( data ) => {
-					expect( data.path ).to.equal( 'tests/foo/file.js' );
+					expect( data.path ).to.equal( path.normalize( 'tests/foo/file.js' ) );
 					done();
 				} )
 			);
 
 			rename.write( new Vinyl( {
 				cwd: './',
-				path: 'tests/foo/file.js',
+				path: path.normalize( 'tests/foo/file.js' ),
 				contents: new Buffer( '' )
 			} ) );
 

+ 0 - 110
dev/tests/dev-boilerplate-update.js

@@ -1,110 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/* global describe, it */
-
-'use strict';
-
-const sinon = require( 'sinon' );
-const tools = require( '../tasks/utils/tools' );
-const git = require( '../tasks/utils/git' );
-const path = require( 'path' );
-
-describe( 'dev-boilerplate-update', () => {
-	const task = require( '../tasks/utils/dev-boilerplate-update' );
-	const ckeditor5Path = 'path/to/ckeditor5';
-	const workspaceRoot = '..';
-	const workspaceAbsolutePath = path.join( ckeditor5Path, workspaceRoot );
-	const emptyFn = () => {};
-
-	it( 'should update boilerplate in dev repositories', () => {
-		const dirs = [ 'ckeditor5-core', 'ckeditor5-devtest' ];
-		const getDependenciesSpy = sinon.spy( tools, 'getCKEditorDependencies' );
-		const getDirectoriesStub = sinon.stub( tools, 'getCKE5Directories' ).returns( dirs );
-		const updateStub = sinon.stub( git, 'updateBoilerplate' );
-		const json = {
-			dependencies: {
-				'ckeditor5-core': 'ckeditor/ckeditor5-core',
-				'ckeditor5-devtest': 'ckeditor/ckeditor5-devtest#new-branch',
-				'other-plugin': '1.2.3'
-			}
-		};
-
-		task( ckeditor5Path, json, workspaceRoot, emptyFn, emptyFn );
-
-		getDependenciesSpy.restore();
-		getDirectoriesStub.restore();
-		updateStub.restore();
-
-		sinon.assert.calledTwice( updateStub );
-		sinon.assert.calledWithExactly( updateStub.firstCall, path.join( workspaceAbsolutePath, dirs[ 0 ] ) );
-		sinon.assert.calledWithExactly( updateStub.secondCall, path.join( workspaceAbsolutePath, dirs[ 1 ] ) );
-	} );
-
-	it( 'should not update boilerplate when no dependencies are found', () => {
-		const getDependenciesSpy = sinon.spy( tools, 'getCKEditorDependencies' );
-		const getDirectoriesStub = sinon.stub( tools, 'getCKE5Directories' );
-		const updateStub = sinon.stub( git, 'updateBoilerplate' );
-		const json = {
-			dependencies: {
-				'other-plugin': '1.2.3'
-			}
-		};
-
-		task( ckeditor5Path, json, workspaceRoot, emptyFn, emptyFn );
-
-		getDependenciesSpy.restore();
-		getDirectoriesStub.restore();
-		updateStub.restore();
-
-		sinon.assert.notCalled( updateStub );
-	} );
-
-	it( 'should not update boilerplate when no plugins in dev mode', () => {
-		const getDependenciesSpy = sinon.spy( tools, 'getCKEditorDependencies' );
-		const getDirectoriesStub = sinon.stub( tools, 'getCKE5Directories' ).returns( [] );
-		const updateStub = sinon.stub( git, 'updateBoilerplate' );
-		const json = {
-			dependencies: {
-				'ckeditor5-devtest': 'ckeditor/ckeditor5-devtest#new-branch',
-				'other-plugin': '1.2.3'
-			}
-		};
-
-		task( ckeditor5Path, json, workspaceRoot, emptyFn, emptyFn );
-
-		getDependenciesSpy.restore();
-		getDirectoriesStub.restore();
-		updateStub.restore();
-
-		sinon.assert.notCalled( updateStub );
-	} );
-
-	it( 'should write error message when updating boilerplate is unsuccessful', () => {
-		const dirs = [ 'ckeditor5-core' ];
-		const getDependenciesSpy = sinon.spy( tools, 'getCKEditorDependencies' );
-		const getDirectoriesStub = sinon.stub( tools, 'getCKE5Directories' ).returns( dirs );
-		const error = new Error( 'Error message.' );
-		const updateStub = sinon.stub( git, 'updateBoilerplate' ).throws( error );
-		const json = {
-			dependencies: {
-				'ckeditor5-core': 'ckeditor/ckeditor5-core',
-				'ckeditor5-devtest': 'ckeditor/ckeditor5-devtest#new-branch',
-				'other-plugin': '1.2.3'
-			}
-		};
-		const writeErrorSpy = sinon.spy();
-
-		task( ckeditor5Path, json, workspaceRoot, emptyFn, writeErrorSpy );
-
-		getDependenciesSpy.restore();
-		getDirectoriesStub.restore();
-		updateStub.restore();
-
-		sinon.assert.calledOnce( updateStub );
-		sinon.assert.calledOnce( writeErrorSpy );
-		sinon.assert.calledWithExactly( writeErrorSpy, error );
-	} );
-} );

+ 0 - 91
dev/tests/dev-plugin-create.js

@@ -1,91 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/* global describe, it, beforeEach, afterEach */
-
-'use strict';
-
-const chai = require( 'chai' );
-const sinon = require( 'sinon' );
-const expect = chai.expect;
-const tools = require( '../tasks/utils/tools' );
-const inquiries = require( '../tasks/utils/inquiries' );
-const git = require( '../tasks/utils/git' );
-const path = require( 'path' );
-const emptyFn = () => { };
-let spies;
-
-describe( 'dev-tasks', () => {
-	const mainRepositoryPath = '/path/to/repository';
-	const workspaceRoot = '..';
-	const workspacePath = path.join( mainRepositoryPath, workspaceRoot );
-	const pluginName = 'plugin-name';
-	const pluginVersion = '0.0.1';
-	const gitHubUrl = 'ckeditor5/plugin-name';
-
-	beforeEach( () => createSpies() );
-	afterEach( () => restoreSpies() );
-
-	function createSpies() {
-		spies = {
-			linkDirectories: sinon.stub( tools, 'linkDirectories' ),
-			npmInstall: sinon.stub( tools, 'npmInstall' ),
-			installGitHooks: sinon.stub( tools, 'installGitHooks' ),
-			getPluginName: sinon.stub( inquiries, 'getPluginName' ).returns( new Promise( ( r ) => r( pluginName ) ) ),
-			getPluginVersion: sinon.stub( inquiries, 'getPluginVersion' ).returns( new Promise( ( r ) => r( pluginVersion ) ) ),
-			getPluginGitHubUrl: sinon.stub( inquiries, 'getPluginGitHubUrl' ).returns( new Promise( ( r ) => r( gitHubUrl ) ) ),
-			initializeRepository: sinon.stub( git, 'initializeRepository' ),
-			updateJSONFile: sinon.stub( tools, 'updateJSONFile' ),
-			copyTemplateFiles: sinon.stub( tools, 'copyTemplateFiles' ),
-			initialCommit: sinon.stub( git, 'initialCommit' )
-		};
-	}
-
-	function restoreSpies() {
-		for ( let spy in spies ) {
-			spies[ spy ].restore();
-		}
-	}
-
-	describe( 'dev-plugin-create', () => {
-		const pluginCreateTask = require( '../tasks/utils/dev-plugin-create' );
-		const repositoryPath = path.join( workspacePath, pluginName );
-
-		it( 'should exist', () => expect( pluginCreateTask ).to.be.a( 'function' ) );
-
-		it( 'should create a plugin', () => {
-			return pluginCreateTask( mainRepositoryPath, workspaceRoot, emptyFn ).then( () => {
-				expect( spies.getPluginName.calledOnce ).to.equal( true );
-				expect( spies.getPluginVersion.calledOnce ).to.equal( true );
-				expect( spies.getPluginGitHubUrl.calledOnce ).to.equal( true );
-				expect( spies.initializeRepository.calledOnce ).to.equal( true );
-				expect( spies.initializeRepository.firstCall.args[ 0 ] ).to.equal( repositoryPath );
-				expect( spies.copyTemplateFiles.calledOnce ).to.equal( true );
-				expect( spies.copyTemplateFiles.firstCall.args[ 0 ] ).to.equal( repositoryPath );
-				expect( spies.updateJSONFile.calledTwice ).to.equal( true );
-				expect( spies.updateJSONFile.firstCall.args[ 0 ] ).to.equal( path.join( repositoryPath, 'package.json' ) );
-				let updateFn = spies.updateJSONFile.firstCall.args[ 1 ];
-				let json = updateFn( {} );
-				expect( json.name ).to.equal( pluginName );
-				expect( json.version ).to.equal( pluginVersion );
-				expect( spies.updateJSONFile.secondCall.args[ 0 ] ).to.equal( path.join( mainRepositoryPath, 'package.json' ) );
-				updateFn = spies.updateJSONFile.secondCall.args[ 1 ];
-				json = updateFn( {} );
-				expect( json.dependencies ).to.be.an( 'object' );
-				expect( json.dependencies[ pluginName ] ).to.equal( gitHubUrl );
-				expect( spies.initialCommit.calledOnce ).to.equal( true );
-				expect( spies.initialCommit.firstCall.args[ 0 ] ).to.equal( pluginName );
-				expect( spies.initialCommit.firstCall.args[ 1 ] ).to.equal( repositoryPath );
-				expect( spies.linkDirectories.calledOnce ).to.equal( true );
-				expect( spies.linkDirectories.firstCall.args[ 0 ] ).to.equal( repositoryPath );
-				expect( spies.linkDirectories.firstCall.args[ 1 ] ).to.equal( path.join( mainRepositoryPath, 'node_modules', pluginName ) );
-				expect( spies.npmInstall.calledOnce ).to.equal( true );
-				expect( spies.npmInstall.firstCall.args[ 0 ] ).to.equal( repositoryPath );
-				expect( spies.installGitHooks.calledOnce ).to.equal( true );
-				expect( spies.installGitHooks.firstCall.args[ 0 ] ).to.equal( repositoryPath );
-			} );
-		} );
-	} );
-} );

+ 86 - 0
dev/tests/dev/create-package.js

@@ -0,0 +1,86 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global describe, it, beforeEach, afterEach */
+
+'use strict';
+
+const chai = require( 'chai' );
+const sinon = require( 'sinon' );
+const expect = chai.expect;
+const tools = require( '../../tasks/dev/utils/tools' );
+const inquiries = require( '../../tasks/dev/utils/inquiries' );
+const git = require( '../../tasks/dev/utils/git' );
+const path = require( 'path' );
+
+describe( 'dev-package-create', () => {
+	const emptyFn = () => { };
+	let spies;
+
+	const mainRepositoryPath = '/path/to/repository';
+	const workspaceRoot = '..';
+	const workspacePath = path.join( mainRepositoryPath, workspaceRoot );
+	const packageName = 'package-name';
+	const packageVersion = '0.0.1';
+	const gitHubUrl = 'ckeditor5/package-name';
+
+	beforeEach( () => createSpies() );
+	afterEach( () => restoreSpies() );
+
+	function createSpies() {
+		spies = {
+			linkDirectories: sinon.stub( tools, 'linkDirectories' ),
+			npmInstall: sinon.stub( tools, 'npmInstall' ),
+			getPackageName: sinon.stub( inquiries, 'getPackageName' ).returns( new Promise( ( r ) => r( packageName ) ) ),
+			getPackageVersion: sinon.stub( inquiries, 'getPackageVersion' ).returns( new Promise( ( r ) => r( packageVersion ) ) ),
+			getPackageGitHubUrl: sinon.stub( inquiries, 'getPackageGitHubUrl' ).returns( new Promise( ( r ) => r( gitHubUrl ) ) ),
+			initializeRepository: sinon.stub( git, 'initializeRepository' ),
+			updateJSONFile: sinon.stub( tools, 'updateJSONFile' ),
+			copy: sinon.stub( tools, 'copy' ),
+			initialCommit: sinon.stub( git, 'initialCommit' )
+		};
+	}
+
+	function restoreSpies() {
+		for ( let spy in spies ) {
+			spies[ spy ].restore();
+		}
+	}
+
+	const packageCreateTask = require( '../../tasks/dev/tasks/create-package' );
+	const repositoryPath = path.join( workspacePath, packageName );
+
+	it( 'should exist', () => expect( packageCreateTask ).to.be.a( 'function' ) );
+
+	it( 'should create a package', () => {
+		return packageCreateTask( mainRepositoryPath, workspaceRoot, emptyFn ).then( () => {
+			expect( spies.getPackageName.calledOnce ).to.equal( true );
+			expect( spies.getPackageVersion.calledOnce ).to.equal( true );
+			expect( spies.getPackageGitHubUrl.calledOnce ).to.equal( true );
+			expect( spies.initializeRepository.calledOnce ).to.equal( true );
+			expect( spies.initializeRepository.firstCall.args[ 0 ] ).to.equal( repositoryPath );
+			expect( spies.copy.called ).to.equal( true );
+			expect( spies.updateJSONFile.calledTwice ).to.equal( true );
+			expect( spies.updateJSONFile.firstCall.args[ 0 ] ).to.equal( path.join( repositoryPath, 'package.json' ) );
+			let updateFn = spies.updateJSONFile.firstCall.args[ 1 ];
+			let json = updateFn( {} );
+			expect( json.name ).to.equal( packageName );
+			expect( json.version ).to.equal( packageVersion );
+			expect( spies.updateJSONFile.secondCall.args[ 0 ] ).to.equal( path.join( mainRepositoryPath, 'package.json' ) );
+			updateFn = spies.updateJSONFile.secondCall.args[ 1 ];
+			json = updateFn( {} );
+			expect( json.dependencies ).to.be.an( 'object' );
+			expect( json.dependencies[ packageName ] ).to.equal( gitHubUrl );
+			expect( spies.initialCommit.calledOnce ).to.equal( true );
+			expect( spies.initialCommit.firstCall.args[ 0 ] ).to.equal( packageName );
+			expect( spies.initialCommit.firstCall.args[ 1 ] ).to.equal( repositoryPath );
+			expect( spies.linkDirectories.calledOnce ).to.equal( true );
+			expect( spies.linkDirectories.firstCall.args[ 0 ] ).to.equal( repositoryPath );
+			expect( spies.linkDirectories.firstCall.args[ 1 ] ).to.equal( path.join( mainRepositoryPath, 'node_modules', packageName ) );
+			expect( spies.npmInstall.calledOnce ).to.equal( true );
+			expect( spies.npmInstall.firstCall.args[ 0 ] ).to.equal( repositoryPath );
+		} );
+	} );
+} );

+ 14 - 61
dev/tests/git.js

@@ -7,18 +7,20 @@
 
 'use strict';
 
-let toRestore;
-const git = require( '../tasks/utils/git' );
+let sandbox;
+const git = require( '../../tasks/dev/utils/git' );
 const chai = require( 'chai' );
 const sinon = require( 'sinon' );
-const tools = require( '../tasks/utils/tools' );
+const tools = require( '../../tasks/dev/utils/tools' );
 const expect = chai.expect;
 
 describe( 'utils', () => {
-	beforeEach( () => toRestore = [] );
+	beforeEach( () => {
+		sandbox = sinon.sandbox.create();
+	} );
 
 	afterEach( () => {
-		toRestore.forEach( item => item.restore() );
+		sandbox.restore();
 	} );
 
 	describe( 'git', () => {
@@ -128,11 +130,10 @@ describe( 'utils', () => {
 			it( 'should be defined', () => expect( git.cloneRepository ).to.be.a( 'function' ) );
 
 			it( 'should call clone commands', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const workspacePath = '/path/to/workspace/';
 				const urlInfo = git.parseRepositoryUrl( 'git@github.com:ckeditor/ckeditor5-core#new-feature' );
 				const cloneCommands = `cd ${ workspacePath } && git clone ${ urlInfo.server + urlInfo.repository }`;
-				toRestore.push( shExecStub );
 
 				git.cloneRepository( urlInfo, workspacePath );
 
@@ -145,11 +146,10 @@ describe( 'utils', () => {
 			it( 'should be defined', () => expect( git.checkout ).to.be.a( 'function' ) );
 
 			it( 'should call checkout commands', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const repositoryLocation = 'path/to/repository';
 				const branchName = 'branch-to-checkout';
 				const checkoutCommands = `cd ${ repositoryLocation } && git checkout ${ branchName }`;
-				toRestore.push( shExecStub );
 
 				git.checkout( repositoryLocation, branchName );
 
@@ -161,11 +161,10 @@ describe( 'utils', () => {
 		describe( 'pull', () => {
 			it( 'should be defined', () => expect( git.pull ).to.be.a( 'function' ) );
 			it( 'should call pull commands', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const repositoryLocation = 'path/to/repository';
 				const branchName = 'branch-to-pull';
 				const pullCommands = `cd ${ repositoryLocation } && git pull origin ${ branchName }`;
-				toRestore.push( shExecStub );
 
 				git.pull( repositoryLocation, branchName );
 
@@ -177,16 +176,11 @@ describe( 'utils', () => {
 		describe( 'initializeRepository', () => {
 			it( 'should be defined', () => expect( git.initializeRepository ).to.be.a( 'function' ) );
 			it( 'should call initialize commands', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const repositoryLocation = 'path/to/repository';
 				const initializeCommands = [
-					`git init ${ repositoryLocation }`,
-					`cd ${ repositoryLocation }`,
-					`git remote add boilerplate ${ git.BOILERPLATE_REPOSITORY }`,
-					`git fetch boilerplate ${ git.BOILERPLATE_BRANCH }`,
-					`git merge boilerplate/${ git.BOILERPLATE_BRANCH }`
+					`git init ${ repositoryLocation }`
 				];
-				toRestore.push( shExecStub );
 
 				git.initializeRepository( repositoryLocation );
 
@@ -198,10 +192,9 @@ describe( 'utils', () => {
 		describe( 'getStatus', () => {
 			it( 'should be defined', () => expect( git.getStatus ).to.be.a( 'function' ) );
 			it( 'should call status command', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const repositoryLocation = 'path/to/repository';
 				const statusCommands = `cd ${ repositoryLocation } && git status --porcelain -sb`;
-				toRestore.push( shExecStub );
 
 				git.getStatus( repositoryLocation );
 
@@ -210,49 +203,10 @@ describe( 'utils', () => {
 			} );
 		} );
 
-		describe( 'updateBoilerplate', () => {
-			it( 'should be defined', () => expect( git.updateBoilerplate ).to.be.a( 'function' ) );
-			it( 'should fetch and merge boilerplate if remote already exists', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
-				const repositoryLocation = 'path/to/repository';
-				const updateCommands = [
-					`cd ${ repositoryLocation }`,
-					`git fetch boilerplate ${ git.BOILERPLATE_BRANCH }`,
-					`git merge boilerplate/${ git.BOILERPLATE_BRANCH }`
-				];
-				shExecStub.onCall( 0 ).returns( 'origin\nboilerplate' );
-				toRestore.push( shExecStub );
-
-				git.updateBoilerplate( repositoryLocation );
-
-				expect( shExecStub.calledTwice ).to.equal( true );
-				expect( shExecStub.secondCall.args[ 0 ] ).to.equal( updateCommands.join( ' && ' ) );
-			} );
-
-			it( 'should add boilerplate remote if one not exists', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
-				const repositoryLocation = 'path/to/repository';
-				const addRemoteCommands = `cd ${ repositoryLocation } && git remote add boilerplate ${ git.BOILERPLATE_REPOSITORY }`;
-				const updateCommands = [
-					`cd ${ repositoryLocation }`,
-					`git fetch boilerplate ${ git.BOILERPLATE_BRANCH }`,
-					`git merge boilerplate/${ git.BOILERPLATE_BRANCH }`
-				];
-				shExecStub.onCall( 0 ).returns( 'origin\nnew' );
-				toRestore.push( shExecStub );
-
-				git.updateBoilerplate( repositoryLocation );
-
-				expect( shExecStub.calledThrice ).to.equal( true );
-				expect( shExecStub.secondCall.args[ 0 ] ).to.equal( addRemoteCommands );
-				expect( shExecStub.thirdCall.args[ 0 ] ).to.equal( updateCommands.join( ' && ' )  );
-			} );
-		} );
-
 		describe( 'initialCommit', () => {
 			it( 'should be defined', () => expect( git.initialCommit ).to.be.a( 'function' ) );
 			it( 'should execute commit commands', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const pluginName = 'ckeditor5-plugin-name';
 				const repositoryPath = '/path/to/repo';
 				const commitCommands = [
@@ -260,7 +214,6 @@ describe( 'utils', () => {
 					`git add .`,
 					`git commit -m "Initial commit for ${ pluginName }."`
 				];
-				toRestore.push( shExecStub );
 
 				git.initialCommit( pluginName, repositoryPath );
 

+ 2 - 2
dev/tests/dev-init.js

@@ -8,10 +8,10 @@
 'use strict';
 
 const sinon = require( 'sinon' );
-const tools = require( '../tasks/utils/tools' );
+const tools = require( '../../tasks/dev/utils/tools' );
 
 describe( 'dev-init', () => {
-	const initTask = require( '../tasks/utils/dev-init' );
+	const initTask = require( '../../tasks/dev/tasks/init' );
 	const ckeditor5Path = 'path/to/ckeditor5';
 	const workspaceRoot = '..';
 	const emptyFn = () => {};

+ 3 - 10
dev/tests/dev-install.js

@@ -9,9 +9,9 @@
 
 const chai = require( 'chai' );
 const sinon = require( 'sinon' );
-const git = require( '../tasks/utils/git' );
-const tools = require( '../tasks/utils/tools' );
-const installTask = require( '../tasks/utils/dev-install' );
+const git = require( '../../tasks/dev/utils/git' );
+const tools = require( '../../tasks/dev/utils/tools' );
+const installTask = require( '../../tasks/dev/tasks/install' );
 const expect = chai.expect;
 const path = require( 'path' );
 
@@ -34,7 +34,6 @@ describe( 'dev-install', () => {
 		spies.getGitUrlFromNpm = sinon.stub( tools, 'getGitUrlFromNpm' );
 		spies.readPackageName = sinon.stub( tools, 'readPackageName' );
 		spies.npmUninstall = sinon.stub( tools, 'npmUninstall' );
-		spies.installGitHooks = sinon.stub( tools, 'installGitHooks' );
 	} );
 
 	afterEach( () => {
@@ -80,9 +79,6 @@ describe( 'dev-install', () => {
 		const json = updateFn( {} );
 		expect( json.dependencies ).to.be.a( 'object' );
 		expect( json.dependencies[ urlInfo.name ] ).to.equal( repositoryUrl );
-
-		sinon.assert.calledOnce( spies.installGitHooks );
-		sinon.assert.calledWithExactly( spies.installGitHooks, repositoryPath );
 	} );
 
 	it( 'should use npm module name', () => {
@@ -123,9 +119,6 @@ describe( 'dev-install', () => {
 		const json = updateFn( {} );
 		expect( json.dependencies ).to.be.a( 'object' );
 		expect( json.dependencies[ urlInfo.name ] ).to.equal( repositoryUrl );
-
-		sinon.assert.calledOnce( spies.installGitHooks );
-		sinon.assert.calledWithExactly( spies.installGitHooks, repositoryPath );
 	} );
 
 	it( 'should use local relative path', () => {

+ 2 - 2
dev/tests/dev-relink.js

@@ -8,11 +8,11 @@
 'use strict';
 
 const sinon = require( 'sinon' );
-const tools = require( '../tasks/utils/tools' );
+const tools = require( '../../tasks/dev/utils/tools' );
 const path = require( 'path' );
 
 describe( 'dev-relink', () => {
-	const task = require( '../tasks/utils/dev-relink' );
+	const task = require( '../../tasks/dev/tasks/relink' );
 	const ckeditor5Path = 'path/to/ckeditor5';
 	const modulesPath = path.join( ckeditor5Path, 'node_modules' );
 	const workspaceRoot = '..';

+ 3 - 3
dev/tests/dev-status.js

@@ -8,12 +8,12 @@
 'use strict';
 
 const sinon = require( 'sinon' );
-const tools = require( '../tasks/utils/tools' );
-const git = require( '../tasks/utils/git' );
+const tools = require( '../../tasks/dev/utils/tools' );
+const git = require( '../../tasks/dev/utils/git' );
 const path = require( 'path' );
 
 describe( 'dev-status', () => {
-	const statusTask = require( '../tasks/utils/dev-status' );
+	const statusTask = require( '../../tasks/dev/tasks/status' );
 	const ckeditor5Path = 'path/to/ckeditor5';
 	const workspaceRoot = '..';
 	const workspaceAbsolutePath = path.join( ckeditor5Path, workspaceRoot );

+ 86 - 83
dev/tests/tools.js

@@ -10,16 +10,18 @@
 const chai = require( 'chai' );
 const sinon = require( 'sinon' );
 const expect = chai.expect;
-const tools = require( '../tasks/utils/tools' );
+const tools = require( '../../tasks/dev/utils/tools' );
 const path = require( 'path' );
 const fs = require( 'fs' );
-let toRestore;
+let sandbox;
 
 describe( 'utils', () => {
-	beforeEach( () => toRestore = [] );
+	beforeEach( () => {
+		sandbox = sinon.sandbox.create();
+	} );
 
 	afterEach( () => {
-		toRestore.forEach( item => item.restore() );
+		sandbox.restore();
 	} );
 
 	describe( 'tools', () => {
@@ -28,8 +30,7 @@ describe( 'utils', () => {
 
 			it( 'should execute command', () => {
 				const sh = require( 'shelljs' );
-				const execStub = sinon.stub( sh, 'exec' ).returns( { code: 0 } );
-				toRestore.push( execStub );
+				const execStub = sandbox.stub( sh, 'exec' ).returns( { code: 0 } );
 
 				tools.shExec( 'command' );
 
@@ -38,8 +39,7 @@ describe( 'utils', () => {
 
 			it( 'should throw error on unsuccessful call', () => {
 				const sh = require( 'shelljs' );
-				const execStub = sinon.stub( sh, 'exec' ).returns( { code: 1 } );
-				toRestore.push( execStub );
+				const execStub = sandbox.stub( sh, 'exec' ).returns( { code: 1 } );
 
 				expect( () => {
 					tools.shExec( 'command' );
@@ -52,11 +52,10 @@ describe( 'utils', () => {
 			it( 'should be defined', () => expect( tools.linkDirectories ).to.be.a( 'function' ) );
 
 			it( 'should link directories', () => {
-				const isDirectoryStub = sinon.stub( tools, 'isDirectory' ).returns( false );
-				const symlinkStub = sinon.stub( fs, 'symlinkSync' );
+				const isDirectoryStub = sandbox.stub( tools, 'isDirectory' ).returns( false );
+				const symlinkStub = sandbox.stub( fs, 'symlinkSync' );
 				const source = '/source/dir';
 				const destination = '/destination/dir';
-				toRestore.push( symlinkStub, isDirectoryStub );
 
 				tools.linkDirectories( source, destination );
 
@@ -67,12 +66,11 @@ describe( 'utils', () => {
 			} );
 
 			it( 'should remove destination directory before linking', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
-				const isDirectoryStub = sinon.stub( tools, 'isDirectory' ).returns( true );
-				const symlinkStub = sinon.stub( fs, 'symlinkSync' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
+				const isDirectoryStub = sandbox.stub( tools, 'isDirectory' ).returns( true );
+				const symlinkStub = sandbox.stub( fs, 'symlinkSync' );
 				const source = '/source/dir';
 				const destination = '/destination/dir';
-				toRestore.push( symlinkStub, shExecStub, isDirectoryStub );
 
 				tools.linkDirectories( source, destination );
 
@@ -119,10 +117,9 @@ describe( 'utils', () => {
 			it( 'should get directories in specified path', () => {
 				const fs = require( 'fs' );
 				const directories = [ 'dir1', 'dir2', 'dir3' ];
-				const readdirSyncStub = sinon.stub( fs, 'readdirSync', () => directories );
-				const isDirectoryStub = sinon.stub( tools, 'isDirectory' ).returns( true );
+				const readdirSyncStub = sandbox.stub( fs, 'readdirSync', () => directories );
+				const isDirectoryStub = sandbox.stub( tools, 'isDirectory' ).returns( true );
 				const dirPath = 'path';
-				toRestore.push( readdirSyncStub, isDirectoryStub );
 
 				tools.getDirectories( dirPath );
 
@@ -139,9 +136,8 @@ describe( 'utils', () => {
 
 			it( 'should return true if path points to directory', () => {
 				const fs = require( 'fs' );
-				const statSyncStub = sinon.stub( fs, 'statSync', () => ( { isDirectory: () => true } ) );
+				const statSyncStub = sandbox.stub( fs, 'statSync', () => ( { isDirectory: () => true } ) );
 				const path = 'path';
-				toRestore.push( statSyncStub );
 
 				const result = tools.isDirectory( path );
 
@@ -152,9 +148,8 @@ describe( 'utils', () => {
 
 			it( 'should return false if path does not point to directory', () => {
 				const fs = require( 'fs' );
-				const statSyncStub = sinon.stub( fs, 'statSync', () => ( { isDirectory: () => false } ) );
+				const statSyncStub = sandbox.stub( fs, 'statSync', () => ( { isDirectory: () => false } ) );
 				const path = 'path';
-				toRestore.push( statSyncStub );
 
 				const result = tools.isDirectory( path );
 
@@ -165,9 +160,8 @@ describe( 'utils', () => {
 
 			it( 'should return false if statSync method throws', () => {
 				const fs = require( 'fs' );
-				const statSyncStub = sinon.stub( fs, 'statSync' ).throws();
+				const statSyncStub = sandbox.stub( fs, 'statSync' ).throws();
 				const path = 'path';
-				toRestore.push( statSyncStub );
 
 				const result = tools.isDirectory( path );
 
@@ -182,9 +176,8 @@ describe( 'utils', () => {
 
 			it( 'should return true if path points to file', () => {
 				const fs = require( 'fs' );
-				const statSyncStub = sinon.stub( fs, 'statSync', () => ( { isFile: () => true } ) );
+				const statSyncStub = sandbox.stub( fs, 'statSync', () => ( { isFile: () => true } ) );
 				const path = 'path';
-				toRestore.push( statSyncStub );
 
 				const result = tools.isFile( path );
 
@@ -195,9 +188,8 @@ describe( 'utils', () => {
 
 			it( 'should return false if path does not point to directory', () => {
 				const fs = require( 'fs' );
-				const statSyncStub = sinon.stub( fs, 'statSync', () => ( { isFile: () => false } ) );
+				const statSyncStub = sandbox.stub( fs, 'statSync', () => ( { isFile: () => false } ) );
 				const path = 'path';
-				toRestore.push( statSyncStub );
 
 				const result = tools.isFile( path );
 
@@ -208,9 +200,8 @@ describe( 'utils', () => {
 
 			it( 'should return false if statSync method throws', () => {
 				const fs = require( 'fs' );
-				const statSyncStub = sinon.stub( fs, 'statSync' ).throws();
+				const statSyncStub = sandbox.stub( fs, 'statSync' ).throws();
 				const path = 'path';
-				toRestore.push( statSyncStub );
 
 				const result = tools.isFile( path );
 
@@ -226,8 +217,7 @@ describe( 'utils', () => {
 			it( 'should return only ckeditor5 directories', () => {
 				const workspacePath = '/workspace/path';
 				const sourceDirectories = [ 'tools', 'ckeditor5', 'ckeditor5-core', '.bin', 'ckeditor5-plugin-image' ];
-				const getDirectoriesStub = sinon.stub( tools, 'getDirectories', () => sourceDirectories );
-				toRestore.push( getDirectoriesStub );
+				sandbox.stub( tools, 'getDirectories', () => sourceDirectories );
 				const directories = tools.getCKE5Directories( workspacePath );
 
 				expect( directories.length ).equal( 2 );
@@ -241,10 +231,9 @@ describe( 'utils', () => {
 			it( 'should read, update and save JSON file', () => {
 				const path = 'path/to/file.json';
 				const fs = require( 'fs' );
-				const readFileStub = sinon.stub( fs, 'readFileSync', () => '{}' );
+				const readFileStub = sandbox.stub( fs, 'readFileSync', () => '{}' );
 				const modifiedJSON = { modified: true };
-				const writeFileStub = sinon.stub( fs, 'writeFileSync' );
-				toRestore.push( readFileStub, writeFileStub );
+				const writeFileStub = sandbox.stub( fs, 'writeFileSync' );
 
 				tools.updateJSONFile( path, () => {
 					return modifiedJSON;
@@ -254,18 +243,34 @@ describe( 'utils', () => {
 				expect( readFileStub.firstCall.args[ 0 ] ).to.equal( path );
 				expect( writeFileStub.calledOnce ).to.equal( true );
 				expect( writeFileStub.firstCall.args[ 0 ] ).to.equal( path );
-				expect( writeFileStub.firstCall.args[ 1 ] ).to.equal( JSON.stringify( modifiedJSON, null, 2 ) );
+				expect( writeFileStub.firstCall.args[ 1 ] ).to.equal( JSON.stringify( modifiedJSON, null, 2 ) + '\n' );
+			} );
+		} );
+
+		describe( 'sortObject', () => {
+			it( 'should be defined', () => expect( tools.sortObject ).to.be.a( 'function' ) );
+			it( 'should reinsert object properties in alphabetical order', () => {
+				let obj = {
+					c: '', d: '', a: '', z: ''
+				};
+
+				const sorted = {
+					a: '', c: '', d: '', z: ''
+				};
+
+				obj = tools.sortObject( obj );
+
+				expect( JSON.stringify( obj ) ).to.equal( JSON.stringify( sorted ) );
 			} );
 		} );
 
 		describe( 'readPackageName', () => {
 			const modulePath = 'path/to/module';
 			it( 'should read package name from NPM module', () => {
-				const isFileStub = sinon.stub( tools, 'isFile' ).returns( true );
+				sandbox.stub( tools, 'isFile' ).returns( true );
 				const fs = require( 'fs' );
 				const name = 'module-name';
-				const readFileStub = sinon.stub( fs, 'readFileSync' ).returns( JSON.stringify( { name: name } ) );
-				toRestore.push( isFileStub, readFileStub );
+				sandbox.stub( fs, 'readFileSync' ).returns( JSON.stringify( { name: name } ) );
 
 				const result = tools.readPackageName( modulePath );
 
@@ -273,8 +278,7 @@ describe( 'utils', () => {
 			} );
 
 			it( 'should return null if no package.json is found', () => {
-				const isFileStub = sinon.stub( tools, 'isFile' ).returns( false );
-				toRestore.push( isFileStub );
+				sandbox.stub( tools, 'isFile' ).returns( false );
 
 				const result = tools.readPackageName( modulePath );
 
@@ -282,10 +286,9 @@ describe( 'utils', () => {
 			} );
 
 			it( 'should return null if no name in package.json is provided', () => {
-				const isFileStub = sinon.stub( tools, 'isFile' ).returns( true );
+				sandbox.stub( tools, 'isFile' ).returns( true );
 				const fs = require( 'fs' );
-				const readFileStub = sinon.stub( fs, 'readFileSync' ).returns( JSON.stringify( { } ) );
-				toRestore.push( isFileStub, readFileStub );
+				sandbox.stub( fs, 'readFileSync' ).returns( JSON.stringify( { } ) );
 
 				const result = tools.readPackageName( modulePath );
 
@@ -296,9 +299,8 @@ describe( 'utils', () => {
 		describe( 'npmInstall', () => {
 			it( 'should be defined', () => expect( tools.npmInstall ).to.be.a( 'function' ) );
 			it( 'should execute npm install command', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const path = '/path/to/repository';
-				toRestore.push( shExecStub );
 
 				tools.npmInstall( path );
 
@@ -310,9 +312,8 @@ describe( 'utils', () => {
 		describe( 'npmUpdate', () => {
 			it( 'should be defined', () => expect( tools.npmUpdate ).to.be.a( 'function' ) );
 			it( 'should execute npm update command', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const path = '/path/to/repository';
-				toRestore.push( shExecStub );
 
 				tools.npmUpdate( path );
 
@@ -324,10 +325,9 @@ describe( 'utils', () => {
 		describe( 'npmUninstall', () => {
 			it( 'should be defined', () => expect( tools.npmUninstall ).to.be.a( 'function' ) );
 			it( 'should execute npm uninstall command', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
+				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const path = '/path/to/repository';
 				const moduleName = 'module-name';
-				toRestore.push( shExecStub );
 
 				tools.npmUninstall( path, moduleName );
 
@@ -336,34 +336,44 @@ describe( 'utils', () => {
 			} );
 		} );
 
-		describe( 'installGitHooks', () => {
-			it( 'should be defined', () => expect( tools.installGitHooks ).to.be.a( 'function' ) );
-			it( 'should execute grunt githooks command', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' );
-				const path = '/path/to/repository';
-				toRestore.push( shExecStub );
+		describe( 'copy', () => {
+			it( 'should be defined', () => expect( tools.copy ).to.be.a( 'function' ) );
+			it( 'should copy files', () => {
+				const fs = require( 'fs-extra' );
+				const path = require( 'path' );
+				let destination = 'destination';
+				let file = 'file.js';
+				sandbox.stub( fs, 'ensureDirSync' );
+				const copyStub = sandbox.stub( fs, 'copySync' );
+				sandbox.stub( tools, 'isFile', () => true );
+				sandbox.stub( tools, 'isDirectory', () => false );
 
-				tools.installGitHooks( path );
+				tools.copy( [ file ], destination );
 
-				expect( shExecStub.calledOnce ).to.equal( true );
-				expect( shExecStub.firstCall.args[ 0 ] ).to.equal( `cd ${ path } && grunt githooks` );
+				file = path.resolve( file );
+				destination = path.join( path.resolve( destination ), path.basename( file ) );
+
+				sinon.assert.calledOnce( copyStub );
+				sinon.assert.calledWithExactly( copyStub.firstCall, file, destination );
 			} );
-		} );
 
-		describe( 'copyTemplateFiles', () => {
-			it( 'should be defined', () => expect( tools.copyTemplateFiles ).to.be.a( 'function' ) );
-			it( 'should copy template files', () => {
+			it( 'should copy directories', () => {
+				const fs = require( 'fs-extra' );
 				const path = require( 'path' );
-				const TEMPLATE_PATH = './dev/tasks/templates';
-				const templatesPath = path.resolve( TEMPLATE_PATH );
-				const shExecStub = sinon.stub( tools, 'shExec' );
-				const repositoryPath = '/path/to/repository';
-				toRestore.push( shExecStub );
+				let destination = 'destination';
+				let dir = 'source';
+				sandbox.stub( fs, 'ensureDirSync' );
+				const copyStub = sandbox.stub( fs, 'copySync' );
+				sandbox.stub( tools, 'isFile', () => false );
+				sandbox.stub( tools, 'isDirectory', () => true );
 
-				tools.copyTemplateFiles( repositoryPath );
+				tools.copy( [ dir ], destination );
 
-				expect( shExecStub.calledOnce ).to.equal( true );
-				expect( shExecStub.firstCall.args[ 0 ] ).to.equal( `cp ${ path.join( templatesPath, '*.md' ) } ${ repositoryPath }` );
+				dir = path.resolve( dir );
+				destination = path.resolve( destination );
+
+				sinon.assert.calledOnce( copyStub );
+				sinon.assert.calledWithExactly( copyStub.firstCall, dir, destination );
 			} );
 		} );
 
@@ -376,10 +386,9 @@ describe( 'utils', () => {
 
 			it( 'should be defined', () => expect( tools.getGitUrlFromNpm ).to.be.a( 'function' ) );
 			it( 'should call npm view command', () => {
-				const shExecStub = sinon.stub( tools, 'shExec', () => {
+				const shExecStub = sandbox.stub( tools, 'shExec', () => {
 					return JSON.stringify( repository );
 				} );
-				toRestore.push( shExecStub );
 				const url = tools.getGitUrlFromNpm( moduleName );
 
 				expect( shExecStub.calledOnce ).to.equal( true );
@@ -388,27 +397,21 @@ describe( 'utils', () => {
 			} );
 
 			it( 'should return null if module is not found', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' ).throws( new Error( 'npm ERR! code E404' ) );
-				toRestore.push( shExecStub );
-
+				sandbox.stub( tools, 'shExec' ).throws( new Error( 'npm ERR! code E404' ) );
 				const url = tools.getGitUrlFromNpm( moduleName );
 				expect( url ).to.equal( null );
 			} );
 
 			it( 'should return null if module has no repository information', () => {
-				const shExecStub = sinon.stub( tools, 'shExec' ).returns( JSON.stringify( {} ) );
-				toRestore.push( shExecStub );
-
+				sandbox.stub( tools, 'shExec' ).returns( JSON.stringify( {} ) );
 				const url = tools.getGitUrlFromNpm( moduleName );
 				expect( url ).to.equal( null );
 			} );
 
 			it( 'should throw on other errors', () => {
 				const error = new Error( 'Random error.' );
-				const shExecStub = sinon.stub( tools, 'shExec' ).throws( error );
-				const getUrlSpy = sinon.spy( tools, 'getGitUrlFromNpm' );
-				toRestore.push( shExecStub );
-				toRestore.push( getUrlSpy );
+				sandbox.stub( tools, 'shExec' ).throws( error );
+				const getUrlSpy = sandbox.spy( tools, 'getGitUrlFromNpm' );
 
 				try {
 					tools.getGitUrlFromNpm( moduleName );

+ 3 - 3
dev/tests/dev-update.js

@@ -8,12 +8,12 @@
 'use strict';
 
 const sinon = require( 'sinon' );
-const tools = require( '../tasks/utils/tools' );
-const git = require( '../tasks/utils/git' );
+const tools = require( '../../tasks/dev/utils/tools' );
+const git = require( '../../tasks/dev/utils/git' );
 const path = require( 'path' );
 
 describe( 'dev-update', () => {
-	const updateTask = require( '../tasks/utils/dev-update' );
+	const updateTask = require( '../../tasks/dev/tasks/update' );
 	const ckeditor5Path = 'path/to/ckeditor5';
 	const workspaceRoot = '..';
 	const workspaceAbsolutePath = path.join( ckeditor5Path, workspaceRoot );

+ 130 - 0
dev/tests/lint.js

@@ -0,0 +1,130 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global describe, it, beforeEach, afterEach */
+
+const sinon = require( 'sinon' );
+const gulp = require( 'gulp' );
+const gutil = require( 'gulp-util' );
+const stream = require( 'stream' );
+const Vinyl = require( 'vinyl' );
+const jshint = require( 'gulp-jshint' );
+const jscs = require( 'gulp-jscs' );
+const concat = require( 'concat-stream' );
+const chai = require( 'chai' );
+const expect = chai.expect;
+const mockery = require( 'mockery' );
+const through = require( 'through2' );
+
+describe( 'lint', () => {
+	'use strict';
+
+	const config = {
+		ROOT_DIR: '.',
+		DIST_DIR: 'dist',
+		IGNORED_FILES: [ 'lib/**' ]
+	};
+
+	let sandbox;
+
+	beforeEach( () => {
+		mockery.enable( {
+			warnOnReplace: false,
+			warnOnUnregistered: false
+		} );
+
+		mockery.registerMock( 'git-guppy', () => {
+			return {
+				stream() {
+					const files = [
+						new Vinyl( {
+							cwd: './',
+							path: './ckeditor.js',
+							contents: new Buffer( 'function test () {};var a;' )
+						} )
+					];
+					const fakeInputStream = new stream.Readable( { objectMode: true } );
+					fakeInputStream._read = () => {
+						fakeInputStream.push( files.pop() || null );
+					};
+
+					return fakeInputStream;
+				}
+			};
+		} );
+
+		sandbox = sinon.sandbox.create();
+	} );
+
+	afterEach( () => {
+		mockery.disable();
+		sandbox.restore();
+	} );
+
+	describe( 'lint()', () => {
+		it( 'should use jshint and jscs on source files', ( done ) => {
+			const files = [
+				new Vinyl( {
+					cwd: './',
+					path: './ckeditor.js',
+					contents: new Buffer( 'function test () {};var a;' )
+				} )
+			];
+
+			const tasks = require( '../tasks/lint/tasks' )( config );
+			const PassThrough = stream.PassThrough;
+
+			sandbox.stub( gulp, 'src', () => {
+				const fakeInputStream = new stream.Readable( { objectMode: true } );
+				fakeInputStream._read = () => {
+					fakeInputStream.push( files.pop() || null );
+				};
+
+				return fakeInputStream;
+			} );
+
+			sandbox.stub( jscs, 'reporter', () => new PassThrough( { objectMode: true } ) );
+			sandbox.stub( jshint, 'reporter', () => new PassThrough( { objectMode: true } ) );
+
+			tasks.lint().pipe( concat( ( data ) => {
+				expect( data.length ).to.equal( 1 );
+				const file = data[ 0 ];
+				expect( typeof file.jscs ).to.equal( 'object' );
+				expect( typeof file.jshint ).to.equal( 'object' );
+				expect( file.jscs.success ).to.equal( false );
+				expect( file.jshint.success ).to.equal( false );
+				done();
+			} ) );
+		} );
+	} );
+
+	describe( 'lintStaged', () => {
+		it( 'should throw error when linting fails', ( done ) => {
+			const tasks = require( '../tasks/lint/tasks' )( config );
+			const PassThrough = stream.PassThrough;
+
+			const exitStub = sandbox.stub( process, 'exit' );
+			sandbox.stub( gutil, 'log' );
+			sandbox.stub( jscs, 'reporter', ( type ) => {
+				if ( type == 'fail' ) {
+					// Fail reporter should report error to stop linting process.
+					return through( { objectMode: true }, ( file, encoding, cb ) => {
+						cb( new Error() );
+						expect( typeof file.jscs ).to.equal( 'object' );
+						expect( typeof file.jshint ).to.equal( 'object' );
+						expect( file.jscs.success ).to.equal( false );
+						expect( file.jshint.success ).to.equal( false );
+						sinon.assert.calledOnce( exitStub );
+						done();
+					} );
+				} else {
+					return new PassThrough( { objectMode: true } );
+				}
+			} );
+			sandbox.stub( jshint, 'reporter', () => new PassThrough( { objectMode: true } ) );
+			tasks.lintStaged();
+		} );
+	} );
+} );

+ 0 - 51
gruntfile.js

@@ -1,51 +0,0 @@
-/* jshint node: true, esnext: true, varstmt: true */
-
-'use strict';
-
-const tools = require( './dev/tasks/utils/tools' );
-
-module.exports = ( grunt ) => {
-	// First register the "default" task, so it can be analyzed by other tasks.
-	grunt.registerTask( 'default', [ 'jshint:git', 'jscs:git' ] );
-
-	// Files that will be ignored by the "jscs" and "jshint" tasks.
-	const ignoreFiles = [
-		// Automatically loaded from .gitignore. Add more if necessary.
-		'lib/**'
-	];
-
-	// Basic configuration which will be overloaded by the tasks.
-	grunt.initConfig( {
-		pkg: grunt.file.readJSON( 'package.json' ),
-		workspaceRoot: '..',
-
-		jshint: {
-			options: {
-				ignores: ignoreFiles
-			}
-		},
-
-		jscs: {
-			options: {
-				excludeFiles: ignoreFiles
-			}
-		},
-
-		replace: {
-			copyright: {
-				src: [ '**/*.*', '**/*.frag' ].concat( tools.getGitIgnore( grunt ).map( i => '!' + i ) )  ,
-				overwrite: true,
-				replacements: [
-					{
-						from: /\@license Copyright \(c\) 2003-\d{4}, CKSource - Frederico Knabben\./,
-						to: '@license Copyright (c) 2003-<%= grunt.template.today("yyyy") %>, CKSource - Frederico Knabben.'
-					}
-				]
-			}
-		}
-	} );
-
-	// Finally load the tasks.
-	grunt.loadTasks( 'dev/tasks' );
-	grunt.loadNpmTasks( 'grunt-text-replace' );
-};

+ 12 - 3
gulpfile.js

@@ -1,4 +1,4 @@
-/* jshint node: true, esnext: true */
+/* jshint node: true */
 
 'use strict';
 
@@ -6,9 +6,18 @@ const gulp = require( 'gulp' );
 
 const config = {
 	ROOT_DIR: '.',
-	DIST_DIR: 'dist'
+	DIST_DIR: 'dist',
+	WORKSPACE_DIR: '..',
+
+	// Files ignored by jshint and jscs tasks. Files from .gitignore will be added automatically during tasks execution.
+	IGNORED_FILES: [
+		'src/lib/**'
+	]
 };
 
-require( './dev/tasks/gulp/build/tasks' )( config );
+require( './dev/tasks/build/tasks' )( config );
+require( './dev/tasks/dev/tasks' )( config );
+require( './dev/tasks/lint/tasks' )( config );
 
 gulp.task( 'default', [ 'build' ] );
+gulp.task( 'pre-commit', [ 'lint-staged' ] );

+ 8 - 5
package.json

@@ -23,22 +23,25 @@
     "benderjs-promise": "^0.1.0",
     "benderjs-sinon": "^0.3.0",
     "chai": "^3.4.0",
+    "concat-stream": "^1.5.1",
     "del": "^2.0.2",
-    "grunt": "^0",
-    "grunt-contrib-jshint": "^0",
-    "grunt-githooks": "^0",
-    "grunt-jscs": "^2.0.0",
-    "grunt-text-replace": "^0.4.0",
+    "fs-extra": "^0.26.4",
+    "git-guppy": "^1.0.1",
     "gulp": "^3.9.0",
     "gulp-babel": "^6.1.0",
     "gulp-filter": "^3.0.1",
+    "gulp-jscs": "^3.0.2",
+    "gulp-jshint": "^2.0.0",
     "gulp-mirror": "^0.4.0",
     "gulp-rename": "^1.2.2",
     "gulp-sourcemaps": "^1.6.0",
     "gulp-util": "^3.0.7",
     "gulp-watch": "^4.3.5",
+    "guppy-pre-commit": "^0.3.0",
     "inquirer": "^0.11.0",
     "istanbul": "^0.4.1",
+    "jshint": "^2.9.1",
+    "jshint-reporter-jscs": "^0.1.0",
     "merge-stream": "^1.0.0",
     "minimist": "^1.2.0",
     "mocha": "^2.2.5",

+ 2 - 2
tests/.jshintrc

@@ -2,15 +2,15 @@
 	"browser": true,
 	"esnext": true,
 
+	"expr": true,
 	"immed": true,
 	"latedef": "nofunc",
 	"loopfunc": true,
 	"noarg": true,
 	"nonbsp": true,
+	"strict": "global",
 	"undef": true,
 	"unused": true,
-	"strict": "global",
-	"expr": true,
 	"varstmt": true,
 
 	"globals": {