8
0
Просмотр исходного кода

Merge pull request #110 from ckeditor/t/93

Update development task changes.
Piotrek Koszuliński 10 лет назад
Родитель
Сommit
14f0a6d44f

+ 6 - 2
dev/tasks/dev/tasks.js

@@ -14,13 +14,17 @@ const pluginCreateTask = require( './tasks/create-package' );
 const updateTask = require( './tasks/update' );
 const updateTask = require( './tasks/update' );
 const relinkTask = require( './tasks/relink' );
 const relinkTask = require( './tasks/relink' );
 const log = require( './utils/log' );
 const log = require( './utils/log' );
+const gutil = require( 'gulp-util' );
 
 
 module.exports = ( config ) => {
 module.exports = ( config ) => {
 	const ckeditor5Path = process.cwd();
 	const ckeditor5Path = process.cwd();
 	const packageJSON = require( '../../../package.json' );
 	const packageJSON = require( '../../../package.json' );
 
 
 	// Configure logging.
 	// Configure logging.
-	log.configure( console.log, console.error );
+	log.configure(
+		( msg ) => gutil.log( msg ),
+		( msg ) => gutil.log( gutil.colors.red( msg ) )
+	);
 
 
 	gulp.task( 'init', () => {
 	gulp.task( 'init', () => {
 		initTask( installTask, ckeditor5Path, packageJSON, config.WORKSPACE_DIR );
 		initTask( installTask, ckeditor5Path, packageJSON, config.WORKSPACE_DIR );
@@ -40,7 +44,7 @@ module.exports = ( config ) => {
 			}
 			}
 		} );
 		} );
 
 
-		updateTask( ckeditor5Path, packageJSON, config.WORKSPACE_DIR, options[ 'npm-update' ] );
+		updateTask( installTask, ckeditor5Path, packageJSON, config.WORKSPACE_DIR, options[ 'npm-update' ] );
 	} );
 	} );
 
 
 	gulp.task( 'status', () => {
 	gulp.task( 'status', () => {

+ 2 - 8
dev/tasks/dev/tasks/install.js

@@ -20,9 +20,8 @@ const log = require( '../utils/log' );
  * 2. If NPM module name is provided - gets GitHub URL from NPM and 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.
  * 3. If path on disk is provided - it is used directly.
  * 4. Runs `npm install` in package repository.
  * 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`.
+ * 5. Links package directory into `ckeditor5/node_modules/`.
+ * 6. Adds dependency to `ckeditor5/package.json`.
  *
  *
  * @param {String} ckeditor5Path Absolute path to `ckeditor5` repository.
  * @param {String} ckeditor5Path Absolute path to `ckeditor5` repository.
  * @param {String} workspaceRoot Relative path to workspace root directory.
  * @param {String} workspaceRoot Relative path to workspace root directory.
@@ -89,11 +88,6 @@ module.exports = ( ckeditor5Path, workspaceRoot, name ) => {
 
 
 		const linkPath = path.join( ckeditor5Path, 'node_modules', urlInfo.name );
 		const linkPath = path.join( ckeditor5Path, 'node_modules', urlInfo.name );
 
 
-		if ( tools.isDirectory( linkPath ) ) {
-			log.out( `Uninstalling ${ urlInfo.name } from CKEditor5 node_modules...` );
-			tools.npmUninstall( ckeditor5Path, urlInfo.name );
-		}
-
 		log.out( `Linking ${ linkPath } to ${ repositoryPath }...` );
 		log.out( `Linking ${ linkPath } to ${ repositoryPath }...` );
 		tools.linkDirectories( repositoryPath, linkPath );
 		tools.linkDirectories( repositoryPath, linkPath );
 
 

+ 48 - 25
dev/tasks/dev/tasks/update.js

@@ -11,17 +11,24 @@ const path = require( 'path' );
 const log = require( '../utils/log' );
 const log = require( '../utils/log' );
 
 
 /**
 /**
- * 1. Get CKEditor5 dependencies from package.json file.
- * 2. Scan workspace for repositories that match dependencies from package.json file.
- * 3. Run GIT pull command on each repository found.
+ * 1. Get CKEditor 5 dependencies from package.json in main CKEditor 5 repository.
+ * 2. If dependency's repository is already cloned in workspace:
+ *		2.1. Fetch and checkout to specified branch.
+ *		2.2. Pull changes to that branch.
+ *		2.3. if --npm-update was specified run npm update --dev in that repository.
+ *		2.4. Recreate symbolic link between repo and main node_modules.
+ * 3. If dependency's repository is not cloned yet - run gulp install on this dependency.
+ * 4. Remove symbolic links to dependencies that are not used in current package.json configuration.
+ * 5. if --npm-update was specified run npm update --dev in main CKeditor 5 repository.
  *
  *
+ * @param {Function} installTask Install task to use on each dependency that is missing from workspace.
  * @param {String} ckeditor5Path Path to main CKEditor5 repository.
  * @param {String} ckeditor5Path Path to main CKEditor5 repository.
  * @param {Object} packageJSON Parsed package.json file from CKEditor5 repository.
  * @param {Object} packageJSON Parsed package.json file from CKEditor5 repository.
  * @param {String} workspaceRoot Relative path to workspace root.
  * @param {String} workspaceRoot Relative path to workspace root.
  * @param {Boolean} runNpmUpdate When set to true `npm update` will be executed inside each plugin repository
  * @param {Boolean} runNpmUpdate When set to true `npm update` will be executed inside each plugin repository
  * and inside CKEditor 5 repository.
  * and inside CKEditor 5 repository.
  */
  */
-module.exports = ( ckeditor5Path, packageJSON, workspaceRoot, runNpmUpdate ) => {
+module.exports = ( installTask, ckeditor5Path, packageJSON, workspaceRoot, runNpmUpdate ) => {
 	const workspaceAbsolutePath = path.join( ckeditor5Path, workspaceRoot );
 	const workspaceAbsolutePath = path.join( ckeditor5Path, workspaceRoot );
 
 
 	// Get all CKEditor dependencies from package.json.
 	// Get all CKEditor dependencies from package.json.
@@ -30,35 +37,51 @@ module.exports = ( ckeditor5Path, packageJSON, workspaceRoot, runNpmUpdate ) =>
 	if ( dependencies ) {
 	if ( dependencies ) {
 		const directories = tools.getCKE5Directories( workspaceAbsolutePath );
 		const directories = tools.getCKE5Directories( workspaceAbsolutePath );
 
 
-		if ( directories.length ) {
-			for ( let dependency in dependencies ) {
-				const repositoryURL = dependencies[ dependency ];
-				const urlInfo = git.parseRepositoryUrl( repositoryURL );
-				const repositoryAbsolutePath = path.join( workspaceAbsolutePath, dependency );
+		for ( let dependency in dependencies ) {
+			const repositoryURL = dependencies[ dependency ];
+			const urlInfo = git.parseRepositoryUrl( repositoryURL );
+			const repositoryAbsolutePath = path.join( workspaceAbsolutePath, dependency );
 
 
-				// Check if repository's directory already exists.
-				if ( directories.indexOf( urlInfo.name ) > -1 ) {
-					log.out( `Checking out ${ urlInfo.name } to ${ urlInfo.branch }...` );
-					git.checkout( repositoryAbsolutePath, urlInfo.branch );
+			// Check if repository's directory already exists.
+			if ( directories.indexOf( urlInfo.name ) > -1 ) {
+				log.out( `Checking out ${ urlInfo.name } to ${ urlInfo.branch }...` );
+				git.checkout( repositoryAbsolutePath, urlInfo.branch );
 
 
-					log.out( `Pulling changes to ${ urlInfo.name }...` );
-					git.pull( repositoryAbsolutePath, urlInfo.branch );
+				log.out( `Pulling changes to ${ urlInfo.name }...` );
+				git.pull( repositoryAbsolutePath, urlInfo.branch );
 
 
-					if ( runNpmUpdate ) {
-						log.out( `Running "npm update" in ${ urlInfo.name }...` );
-						tools.npmUpdate( repositoryAbsolutePath );
-					}
+				if ( runNpmUpdate ) {
+					log.out( `Running "npm update" in ${ urlInfo.name }...` );
+					tools.npmUpdate( repositoryAbsolutePath );
 				}
 				}
-			}
 
 
-			if ( runNpmUpdate ) {
-				log.out( `Running "npm update" in CKEditor5 repository...` );
-				tools.npmUpdate( ckeditor5Path );
+				try {
+					log.out( `Linking ${ repositoryURL }...` );
+					tools.linkDirectories( repositoryAbsolutePath, path.join( ckeditor5Path, 'node_modules', dependency ) );
+				} catch ( error ) {
+					log.err( error );
+				}
+			} else {
+				// Directory does not exits in workspace - install it.
+				installTask( ckeditor5Path, workspaceRoot, repositoryURL );
 			}
 			}
-		} else {
-			log.out( 'No CKEditor5 plugins in development mode.' );
+		}
+
+		if ( runNpmUpdate ) {
+			log.out( `Running "npm update" in CKEditor5 repository...` );
+			tools.npmUpdate( ckeditor5Path );
 		}
 		}
 	} else {
 	} else {
 		log.out( 'No CKEditor5 dependencies found in package.json file.' );
 		log.out( 'No CKEditor5 dependencies found in package.json file.' );
 	}
 	}
+
+	// Remove symlinks not used in this configuration.
+	const nodeModulesPath = path.join( ckeditor5Path, 'node_modules' );
+	const symlinks = tools.getCKE5Symlinks( nodeModulesPath );
+	symlinks
+		.filter( dir => typeof dependencies[ dir ] == 'undefined' )
+		.forEach( dir => {
+			log.out( `Removing symbolic link to ${ dir }.` );
+			tools.removeSymlink( path.join( nodeModulesPath, dir ) );
+		} );
 };
 };

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

@@ -66,7 +66,7 @@ module.exports = {
 	},
 	},
 
 
 	/**
 	/**
-	 * Checks out branch on selected repository.
+	 * Fetches and checks out branch on selected repository.
 	 *
 	 *
 	 * @param {String} repositoryLocation Absolute path to repository.
 	 * @param {String} repositoryLocation Absolute path to repository.
 	 * @param {String} branchName Name of the branch to checkout.
 	 * @param {String} branchName Name of the branch to checkout.
@@ -74,6 +74,7 @@ module.exports = {
 	checkout( repositoryLocation, branchName ) {
 	checkout( repositoryLocation, branchName ) {
 		const checkoutCommands = [
 		const checkoutCommands = [
 			`cd ${ repositoryLocation }`,
 			`cd ${ repositoryLocation }`,
+			`git fetch origin ${ branchName }`,
 			`git checkout ${ branchName }`
 			`git checkout ${ branchName }`
 		];
 		];
 
 

+ 57 - 11
dev/tasks/dev/utils/tools.js

@@ -5,8 +5,9 @@
 
 
 'use strict';
 'use strict';
 
 
+const gutil = require( 'gulp-util' );
+
 const dependencyRegExp = /^ckeditor5-/;
 const dependencyRegExp = /^ckeditor5-/;
-const log = require( '../utils/log' );
 
 
 module.exports = {
 module.exports = {
 
 
@@ -20,25 +21,25 @@ module.exports = {
 	 */
 	 */
 	shExec( command, logOutput ) {
 	shExec( command, logOutput ) {
 		const sh = require( 'shelljs' );
 		const sh = require( 'shelljs' );
+
 		sh.config.silent = true;
 		sh.config.silent = true;
 		logOutput = logOutput !== false;
 		logOutput = logOutput !== false;
 
 
 		const ret = sh.exec( command );
 		const ret = sh.exec( command );
+		const logColor = ret.code ? gutil.colors.red : gutil.colors.grey;
 
 
 		if ( logOutput ) {
 		if ( logOutput ) {
-			if ( ret.stdout !== '' ) {
-				log.out( ret.stdout );
+			if ( ret.stdout ) {
+				console.log( '\n' + logColor( ret.stdout.trim() ) + '\n' );
 			}
 			}
 
 
-			if ( ret.stderr !== '' ) {
-				log.err( ret.stderr );
+			if ( ret.stderr ) {
+				console.log( '\n' + gutil.colors.grey( ret.stderr.trim() ) + '\n' );
 			}
 			}
 		}
 		}
 
 
 		if ( ret.code ) {
 		if ( ret.code ) {
-			throw new Error(
-				`Error while executing ${ command }: ${ ret.stderr }`
-			);
+			throw new Error( `Error while executing ${ command }: ${ ret.stderr }` );
 		}
 		}
 
 
 		return ret.stdout;
 		return ret.stdout;
@@ -52,7 +53,9 @@ module.exports = {
 	linkDirectories( source, destination ) {
 	linkDirectories( source, destination ) {
 		const fs = require( 'fs' );
 		const fs = require( 'fs' );
 		// Remove destination directory if exists.
 		// Remove destination directory if exists.
-		if ( this.isDirectory( destination ) ) {
+		if ( this.isSymlink( destination ) ) {
+			this.removeSymlink( destination );
+		} else if ( this.isDirectory( destination ) ) {
 			this.shExec( `rm -rf ${ destination }` );
 			this.shExec( `rm -rf ${ destination }` );
 		}
 		}
 
 
@@ -131,6 +134,21 @@ module.exports = {
 		return false;
 		return false;
 	},
 	},
 
 
+	/**
+	 * Returns true if path points to symbolic link.
+	 *
+	 * @param {String} path
+	 */
+	isSymlink( path ) {
+		const fs = require( 'fs' );
+
+		try {
+			return fs.lstatSync( path ).isSymbolicLink();
+		} catch ( e ) {}
+
+		return false;
+	},
+
 	/**
 	/**
 	 * Returns all directories under specified path that match 'ckeditor5' pattern.
 	 * Returns all directories under specified path that match 'ckeditor5' pattern.
 	 *
 	 *
@@ -209,18 +227,19 @@ module.exports = {
 	 * Calls `npm uninstall <name>` command in specified path.
 	 * Calls `npm uninstall <name>` command in specified path.
 	 *
 	 *
 	 * @param {String} path
 	 * @param {String} path
+	 * @param {String} name
 	 */
 	 */
 	npmUninstall( path, name ) {
 	npmUninstall( path, name ) {
 		this.shExec( `cd ${ path } && npm uninstall ${ name }` );
 		this.shExec( `cd ${ path } && npm uninstall ${ name }` );
 	},
 	},
 
 
 	/**
 	/**
-	 * Calls `npm update` command in specified path.
+	 * Calls `npm update --dev` command in specified path.
 	 *
 	 *
 	 * @param {String} path
 	 * @param {String} path
 	 */
 	 */
 	npmUpdate( path ) {
 	npmUpdate( path ) {
-		this.shExec( `cd ${ path } && npm update` );
+		this.shExec( `cd ${ path } && npm update --dev` );
 	},
 	},
 
 
 	/**
 	/**
@@ -279,5 +298,32 @@ module.exports = {
 		}
 		}
 
 
 		return null;
 		return null;
+	},
+
+	/**
+	 * Returns list of symbolic links to directories with names starting with `ckeditor5-` prefix.
+	 *
+	 * @param {String} path Path to directory,
+	 * @returns {Array} Array with directories names.
+	 */
+	getCKE5Symlinks( path ) {
+		const fs = require( 'fs' );
+		const pth = require( 'path' );
+
+		return fs.readdirSync( path ).filter( item => {
+			const fullPath = pth.join( path, item );
+
+			return dependencyRegExp.test( item ) && this.isSymlink( fullPath );
+		} );
+	},
+
+	/**
+	 * Unlinks symbolic link under specified path.
+	 *
+	 * @param {String} path
+	 */
+	removeSymlink( path ) {
+		const fs = require( 'fs' );
+		fs.unlinkSync( path );
 	}
 	}
 };
 };

+ 5 - 1
dev/tests/dev/git.js

@@ -149,7 +149,11 @@ describe( 'utils', () => {
 				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const repositoryLocation = 'path/to/repository';
 				const repositoryLocation = 'path/to/repository';
 				const branchName = 'branch-to-checkout';
 				const branchName = 'branch-to-checkout';
-				const checkoutCommands = `cd ${ repositoryLocation } && git checkout ${ branchName }`;
+				const checkoutCommands = [
+					`cd ${ repositoryLocation }`,
+					`git fetch origin ${ branchName }`,
+					`git checkout ${ branchName }`
+				].join( ' && ' );
 
 
 				git.checkout( repositoryLocation, branchName );
 				git.checkout( repositoryLocation, branchName );
 
 

+ 4 - 8
dev/tests/dev/install.js

@@ -33,7 +33,6 @@ describe( 'dev-install', () => {
 		spies.checkout = sinon.stub( git, 'checkout' );
 		spies.checkout = sinon.stub( git, 'checkout' );
 		spies.getGitUrlFromNpm = sinon.stub( tools, 'getGitUrlFromNpm' );
 		spies.getGitUrlFromNpm = sinon.stub( tools, 'getGitUrlFromNpm' );
 		spies.readPackageName = sinon.stub( tools, 'readPackageName' );
 		spies.readPackageName = sinon.stub( tools, 'readPackageName' );
-		spies.npmUninstall = sinon.stub( tools, 'npmUninstall' );
 	} );
 	} );
 
 
 	afterEach( () => {
 	afterEach( () => {
@@ -47,7 +46,7 @@ describe( 'dev-install', () => {
 
 
 		installTask( ckeditor5Path, workspacePath, repositoryUrl );
 		installTask( ckeditor5Path, workspacePath, repositoryUrl );
 
 
-		sinon.assert.calledThrice( spies.isDirectory );
+		sinon.assert.calledTwice( spies.isDirectory );
 		sinon.assert.calledOnce( spies.parseUrl );
 		sinon.assert.calledOnce( spies.parseUrl );
 		sinon.assert.calledWithExactly( spies.parseUrl, repositoryUrl );
 		sinon.assert.calledWithExactly( spies.parseUrl, repositoryUrl );
 
 
@@ -65,9 +64,6 @@ describe( 'dev-install', () => {
 
 
 		const linkPath = path.join( ckeditor5Path, 'node_modules', urlInfo.name );
 		const linkPath = path.join( ckeditor5Path, 'node_modules', urlInfo.name );
 
 
-		sinon.assert.calledOnce( spies.npmUninstall );
-		sinon.assert.calledWithExactly( spies.npmUninstall, ckeditor5Path, urlInfo.name );
-
 		sinon.assert.calledOnce( spies.linkDirectories );
 		sinon.assert.calledOnce( spies.linkDirectories );
 		sinon.assert.calledWithExactly( spies.linkDirectories, repositoryPath, linkPath );
 		sinon.assert.calledWithExactly( spies.linkDirectories, repositoryPath, linkPath );
 
 
@@ -88,7 +84,7 @@ describe( 'dev-install', () => {
 
 
 		installTask( ckeditor5Path, workspacePath, moduleName );
 		installTask( ckeditor5Path, workspacePath, moduleName );
 
 
-		sinon.assert.calledThrice( spies.isDirectory );
+		sinon.assert.calledTwice( spies.isDirectory );
 		sinon.assert.calledTwice( spies.parseUrl );
 		sinon.assert.calledTwice( spies.parseUrl );
 		sinon.assert.calledWithExactly( spies.parseUrl.firstCall, moduleName );
 		sinon.assert.calledWithExactly( spies.parseUrl.firstCall, moduleName );
 		expect( spies.parseUrl.firstCall.returnValue ).to.equal( null );
 		expect( spies.parseUrl.firstCall.returnValue ).to.equal( null );
@@ -128,7 +124,7 @@ describe( 'dev-install', () => {
 
 
 		installTask( ckeditor5Path, workspacePath, '../ckeditor5-core' );
 		installTask( ckeditor5Path, workspacePath, '../ckeditor5-core' );
 
 
-		sinon.assert.calledThrice( spies.isDirectory );
+		sinon.assert.calledTwice( spies.isDirectory );
 		sinon.assert.calledOnce( spies.readPackageName );
 		sinon.assert.calledOnce( spies.readPackageName );
 	} );
 	} );
 
 
@@ -139,7 +135,7 @@ describe( 'dev-install', () => {
 
 
 		installTask( ckeditor5Path, workspacePath, '/ckeditor5-core' );
 		installTask( ckeditor5Path, workspacePath, '/ckeditor5-core' );
 
 
-		sinon.assert.calledThrice( spies.isDirectory );
+		sinon.assert.calledTwice( spies.isDirectory );
 		sinon.assert.calledOnce( spies.readPackageName );
 		sinon.assert.calledOnce( spies.readPackageName );
 	} );
 	} );
 
 

+ 90 - 7
dev/tests/dev/tools.js

@@ -50,16 +50,13 @@ describe( 'utils', () => {
 			it( 'should output using log functions', () => {
 			it( 'should output using log functions', () => {
 				const sh = require( 'shelljs' );
 				const sh = require( 'shelljs' );
 				const execStub = sandbox.stub( sh, 'exec' ).returns( { code: 0, stdout: 'out', stderr: 'err' } );
 				const execStub = sandbox.stub( sh, 'exec' ).returns( { code: 0, stdout: 'out', stderr: 'err' } );
-				const log = require( '../../tasks/dev/utils/log' );
-				const outFn = sandbox.spy();
-				const errFn = sandbox.spy();
-				log.configure( outFn, errFn );
+
+				sandbox.stub( console, 'log' );
 
 
 				tools.shExec( 'command' );
 				tools.shExec( 'command' );
 
 
 				sinon.assert.calledOnce( execStub );
 				sinon.assert.calledOnce( execStub );
-				sinon.assert.calledWithExactly( outFn, 'out' );
-				sinon.assert.calledWithExactly( errFn, 'err' );
+				sinon.assert.calledTwice( console.log );
 			} );
 			} );
 
 
 			it( 'should not log when in silent mode', () => {
 			it( 'should not log when in silent mode', () => {
@@ -76,12 +73,28 @@ describe( 'utils', () => {
 				sinon.assert.notCalled( outFn );
 				sinon.assert.notCalled( outFn );
 				sinon.assert.notCalled( errFn );
 				sinon.assert.notCalled( errFn );
 			} );
 			} );
+
+			it( 'should not log if no output from executed command', () => {
+				const sh = require( 'shelljs' );
+				const execStub = sandbox.stub( sh, 'exec' ).returns( { code: 0, stdout: '', stderr: '' } );
+				const log = require( '../../tasks/dev/utils/log' );
+				const outFn = sandbox.spy();
+				const errFn = sandbox.spy();
+				log.configure( outFn, errFn );
+
+				tools.shExec( 'command' );
+
+				sinon.assert.calledOnce( execStub );
+				sinon.assert.notCalled( outFn );
+				sinon.assert.notCalled( errFn );
+			} );
 		} );
 		} );
 
 
 		describe( 'linkDirectories', () => {
 		describe( 'linkDirectories', () => {
 			it( 'should be defined', () => expect( tools.linkDirectories ).to.be.a( 'function' ) );
 			it( 'should be defined', () => expect( tools.linkDirectories ).to.be.a( 'function' ) );
 
 
 			it( 'should link directories', () => {
 			it( 'should link directories', () => {
+				const isSymlinkStub = sandbox.stub( tools, 'isSymlink' ).returns( false );
 				const isDirectoryStub = sandbox.stub( tools, 'isDirectory' ).returns( false );
 				const isDirectoryStub = sandbox.stub( tools, 'isDirectory' ).returns( false );
 				const symlinkStub = sandbox.stub( fs, 'symlinkSync' );
 				const symlinkStub = sandbox.stub( fs, 'symlinkSync' );
 				const source = '/source/dir';
 				const source = '/source/dir';
@@ -90,6 +103,7 @@ describe( 'utils', () => {
 				tools.linkDirectories( source, destination );
 				tools.linkDirectories( source, destination );
 
 
 				expect( isDirectoryStub.calledOnce ).to.equal( true );
 				expect( isDirectoryStub.calledOnce ).to.equal( true );
+				expect( isSymlinkStub.calledOnce ).to.equal( true );
 				expect( symlinkStub.calledOnce ).to.equal( true );
 				expect( symlinkStub.calledOnce ).to.equal( true );
 				expect( symlinkStub.firstCall.args[ 0 ] ).to.equal( source );
 				expect( symlinkStub.firstCall.args[ 0 ] ).to.equal( source );
 				expect( symlinkStub.firstCall.args[ 1 ] ).to.equal( destination );
 				expect( symlinkStub.firstCall.args[ 1 ] ).to.equal( destination );
@@ -97,6 +111,7 @@ describe( 'utils', () => {
 
 
 			it( 'should remove destination directory before linking', () => {
 			it( 'should remove destination directory before linking', () => {
 				const shExecStub = sandbox.stub( tools, 'shExec' );
 				const shExecStub = sandbox.stub( tools, 'shExec' );
+				const isSymlinkStub = sandbox.stub( tools, 'isSymlink' ).returns( false );
 				const isDirectoryStub = sandbox.stub( tools, 'isDirectory' ).returns( true );
 				const isDirectoryStub = sandbox.stub( tools, 'isDirectory' ).returns( true );
 				const symlinkStub = sandbox.stub( fs, 'symlinkSync' );
 				const symlinkStub = sandbox.stub( fs, 'symlinkSync' );
 				const source = '/source/dir';
 				const source = '/source/dir';
@@ -105,10 +120,31 @@ describe( 'utils', () => {
 				tools.linkDirectories( source, destination );
 				tools.linkDirectories( source, destination );
 
 
 				expect( isDirectoryStub.calledOnce ).to.equal( true );
 				expect( isDirectoryStub.calledOnce ).to.equal( true );
+				expect( isSymlinkStub.calledOnce ).to.equal( true );
 				expect( shExecStub.calledOnce ).to.equal( true );
 				expect( shExecStub.calledOnce ).to.equal( true );
 				expect( symlinkStub.firstCall.args[ 0 ] ).to.equal( source );
 				expect( symlinkStub.firstCall.args[ 0 ] ).to.equal( source );
 				expect( symlinkStub.firstCall.args[ 1 ] ).to.equal( destination );
 				expect( symlinkStub.firstCall.args[ 1 ] ).to.equal( destination );
 			} );
 			} );
+
+			it( 'should unlink destination directory if symlink', () => {
+				const shExecStub = sandbox.stub( tools, 'shExec' );
+				const isSymlinkStub = sandbox.stub( tools, 'isSymlink' ).returns( true );
+				const removeSymlinkStub = sandbox.stub( tools, 'removeSymlink' );
+				const isDirectoryStub = sandbox.stub( tools, 'isDirectory' ).returns( true );
+				const symlinkStub = sandbox.stub( fs, 'symlinkSync' );
+				const source = '/source/dir';
+				const destination = '/destination/dir';
+
+				tools.linkDirectories( source, destination );
+
+				expect( isDirectoryStub.notCalled ).to.equal( true );
+				expect( isSymlinkStub.calledOnce ).to.equal( true );
+				expect( shExecStub.notCalled ).to.equal( true );
+				expect( removeSymlinkStub.calledOnce ).to.equal( true );
+				expect( removeSymlinkStub.firstCall.args[ 0 ] ).to.equal( destination );
+				expect( symlinkStub.firstCall.args[ 0 ] ).to.equal( source );
+				expect( symlinkStub.firstCall.args[ 1 ] ).to.equal( destination );
+			} );
 		} );
 		} );
 
 
 		describe( 'getCKEditorDependencies', () => {
 		describe( 'getCKEditorDependencies', () => {
@@ -241,6 +277,26 @@ describe( 'utils', () => {
 			} );
 			} );
 		} );
 		} );
 
 
+		describe( 'isSymlink', () => {
+			it( 'should return true if path points to symbolic link', () => {
+				const path = 'path/to/file';
+				const fs = require( 'fs' );
+				sandbox.stub( fs, 'lstatSync' ).returns( {
+					isSymbolicLink: () => true
+				} );
+
+				expect( tools.isSymlink( path ) ).to.equal( true );
+			} );
+
+			it( 'should return false if lstatSync throws', () => {
+				const path = 'path/to/file';
+				const fs = require( 'fs' );
+				sandbox.stub( fs, 'lstatSync' ).throws();
+
+				expect( tools.isSymlink( path ) ).to.equal( false );
+			} );
+		} );
+
 		describe( 'getCKE5Directories', () => {
 		describe( 'getCKE5Directories', () => {
 			it( 'should be defined', () => expect( tools.getCKE5Directories ).to.be.a( 'function' ) );
 			it( 'should be defined', () => expect( tools.getCKE5Directories ).to.be.a( 'function' ) );
 
 
@@ -348,7 +404,7 @@ describe( 'utils', () => {
 				tools.npmUpdate( path );
 				tools.npmUpdate( path );
 
 
 				expect( shExecStub.calledOnce ).to.equal( true );
 				expect( shExecStub.calledOnce ).to.equal( true );
-				expect( shExecStub.firstCall.args[ 0 ] ).to.equal( `cd ${ path } && npm update` );
+				expect( shExecStub.firstCall.args[ 0 ] ).to.equal( `cd ${ path } && npm update --dev` );
 			} );
 			} );
 		} );
 		} );
 
 
@@ -441,5 +497,32 @@ describe( 'utils', () => {
 				expect( getUrlSpy.threw( error ) ).to.equal( true );
 				expect( getUrlSpy.threw( error ) ).to.equal( true );
 			} );
 			} );
 		} );
 		} );
+
+		describe( 'getCKE5Symlinks', () => {
+			it( 'should return CKE5 symlinks from provided path', () => {
+				const fs = require( 'fs' );
+				const path = 'path/to/dir';
+				sandbox.stub( fs, 'readdirSync' ).returns( [ 'ckeditor5-core', 'ckeditor5-image', 'other-dependency' ] );
+				sandbox.stub( tools, 'isSymlink' ).returns( true );
+
+				const symlinks = tools.getCKE5Symlinks( path );
+				expect( symlinks.length ).to.equal( 2 );
+				expect( symlinks[ 0 ] ).to.equal( 'ckeditor5-core' );
+				expect( symlinks[ 1 ] ).to.equal( 'ckeditor5-image' );
+			} );
+		} );
+
+		describe( 'removeSymlink', () => {
+			it( 'should unlink provided symlink', () => {
+				const fs = require( 'fs' );
+				const unlinkStub = sandbox.stub( fs, 'unlinkSync' );
+				const path = 'path/to/symlink';
+
+				tools.removeSymlink( path );
+
+				expect( unlinkStub.calledOnce ).to.equal( true );
+				expect( unlinkStub.firstCall.args[ 0 ] ).to.equal( path );
+			} );
+		} );
 	} );
 	} );
 } );
 } );

+ 131 - 15
dev/tests/dev/update.js

@@ -24,6 +24,8 @@ describe( 'dev-update', () => {
 		spies.checkout = sinon.stub( git, 'checkout' );
 		spies.checkout = sinon.stub( git, 'checkout' );
 		spies.pull = sinon.stub( git, 'pull' );
 		spies.pull = sinon.stub( git, 'pull' );
 		spies.npmUpdate = sinon.stub( tools, 'npmUpdate' );
 		spies.npmUpdate = sinon.stub( tools, 'npmUpdate' );
+		spies.linkDirectories = sinon.stub( tools, 'linkDirectories' );
+		spies.removeSymlink = sinon.stub( tools, 'removeSymlink' );
 	} );
 	} );
 
 
 	afterEach( () => {
 	afterEach( () => {
@@ -32,7 +34,9 @@ describe( 'dev-update', () => {
 
 
 	it( 'should update dev repositories', () => {
 	it( 'should update dev repositories', () => {
 		const dirs = [ 'ckeditor5-core', 'ckeditor5-devtest' ];
 		const dirs = [ 'ckeditor5-core', 'ckeditor5-devtest' ];
+		const installTask = sinon.spy();
 		spies.getDirectories = sinon.stub( tools, 'getCKE5Directories' ).returns( dirs );
 		spies.getDirectories = sinon.stub( tools, 'getCKE5Directories' ).returns( dirs );
+		spies.getCKE5Symlinks = sinon.stub( tools, 'getCKE5Symlinks' ).returns( [] );
 
 
 		const json = {
 		const json = {
 			dependencies: {
 			dependencies: {
@@ -42,61 +46,173 @@ describe( 'dev-update', () => {
 			}
 			}
 		};
 		};
 
 
-		updateTask( ckeditor5Path, json, workspaceRoot, true );
+		updateTask( installTask, ckeditor5Path, json, workspaceRoot, true );
 
 
+		const repoPath1 = path.join( workspaceAbsolutePath, dirs[ 0 ] );
+		const repoPath2 = path.join( workspaceAbsolutePath, dirs[ 1 ] );
+
+		sinon.assert.calledTwice( spies.checkout );
+		sinon.assert.calledWithExactly( spies.checkout.firstCall, repoPath1, 'master' );
+		sinon.assert.calledWithExactly( spies.checkout.secondCall, repoPath2, 'new-branch' );
 		sinon.assert.calledTwice( spies.pull );
 		sinon.assert.calledTwice( spies.pull );
-		sinon.assert.calledWithExactly( spies.pull.firstCall, path.join( workspaceAbsolutePath, dirs[ 0 ] ), 'master' );
-		sinon.assert.calledWithExactly( spies.pull.secondCall, path.join( workspaceAbsolutePath, dirs[ 1 ] ), 'new-branch' );
+		sinon.assert.calledWithExactly( spies.pull.firstCall, repoPath1, 'master' );
+		sinon.assert.calledWithExactly( spies.pull.secondCall, repoPath2, 'new-branch' );
 
 
 		sinon.assert.calledThrice( spies.npmUpdate );
 		sinon.assert.calledThrice( spies.npmUpdate );
 		sinon.assert.calledWithExactly( spies.npmUpdate.firstCall, path.join( workspaceAbsolutePath, dirs[ 0 ] ) );
 		sinon.assert.calledWithExactly( spies.npmUpdate.firstCall, path.join( workspaceAbsolutePath, dirs[ 0 ] ) );
 		sinon.assert.calledWithExactly( spies.npmUpdate.secondCall, path.join( workspaceAbsolutePath, dirs[ 1 ] ) );
 		sinon.assert.calledWithExactly( spies.npmUpdate.secondCall, path.join( workspaceAbsolutePath, dirs[ 1 ] ) );
 		sinon.assert.calledWithExactly( spies.npmUpdate.thirdCall, ckeditor5Path );
 		sinon.assert.calledWithExactly( spies.npmUpdate.thirdCall, ckeditor5Path );
+
+		sinon.assert.calledOnce( spies.getCKE5Symlinks );
+		sinon.assert.notCalled( spies.removeSymlink );
+		sinon.assert.notCalled( installTask );
 	} );
 	} );
 
 
-	it( 'should update dev repositories without running npm update', () => {
-		const dirs = [ 'ckeditor5-core' ];
+	it( 'should install missing dependencies', () => {
+		const dirs = [ 'ckeditor5-core', 'ckeditor5-devtest' ];
+		const installTask = sinon.spy();
 		spies.getDirectories = sinon.stub( tools, 'getCKE5Directories' ).returns( dirs );
 		spies.getDirectories = sinon.stub( tools, 'getCKE5Directories' ).returns( dirs );
+		spies.getCKE5Symlinks = sinon.stub( tools, 'getCKE5Symlinks' ).returns( [] );
 
 
 		const json = {
 		const json = {
 			dependencies: {
 			dependencies: {
 				'ckeditor5-core': 'ckeditor/ckeditor5-core',
 				'ckeditor5-core': 'ckeditor/ckeditor5-core',
 				'ckeditor5-devtest': 'ckeditor/ckeditor5-devtest#new-branch',
 				'ckeditor5-devtest': 'ckeditor/ckeditor5-devtest#new-branch',
+				'ckeditor5-ui': 'ckeditor/ckeditor5-ui',
 				'other-plugin': '1.2.3'
 				'other-plugin': '1.2.3'
 			}
 			}
 		};
 		};
 
 
-		updateTask( ckeditor5Path, json, workspaceRoot, false );
-		sinon.assert.calledWithExactly( spies.pull.firstCall, path.join( workspaceAbsolutePath, dirs[ 0 ] ), 'master' );
-		sinon.assert.notCalled( spies.npmUpdate );
+		updateTask( installTask, ckeditor5Path, json, workspaceRoot, true );
+
+		const repoPath1 = path.join( workspaceAbsolutePath, dirs[ 0 ] );
+		const repoPath2 = path.join( workspaceAbsolutePath, dirs[ 1 ] );
+
+		sinon.assert.calledTwice( spies.checkout );
+		sinon.assert.calledWithExactly( spies.checkout.firstCall, repoPath1, 'master' );
+		sinon.assert.calledWithExactly( spies.checkout.secondCall, repoPath2, 'new-branch' );
+		sinon.assert.calledTwice( spies.pull );
+		sinon.assert.calledWithExactly( spies.pull.firstCall, repoPath1, 'master' );
+		sinon.assert.calledWithExactly( spies.pull.secondCall, repoPath2, 'new-branch' );
+
+		sinon.assert.calledThrice( spies.npmUpdate );
+		sinon.assert.calledWithExactly( spies.npmUpdate.firstCall, path.join( workspaceAbsolutePath, dirs[ 0 ] ) );
+		sinon.assert.calledWithExactly( spies.npmUpdate.secondCall, path.join( workspaceAbsolutePath, dirs[ 1 ] ) );
+		sinon.assert.calledWithExactly( spies.npmUpdate.thirdCall, ckeditor5Path );
+
+		sinon.assert.calledOnce( installTask );
+		sinon.assert.calledWithExactly( installTask, ckeditor5Path, workspaceRoot, 'ckeditor/ckeditor5-ui' );
+
+		sinon.assert.calledOnce( spies.getCKE5Symlinks );
+		sinon.assert.notCalled( spies.removeSymlink );
 	} );
 	} );
 
 
-	it( 'should not update when no dependencies are found', () => {
-		spies.getDirectories = sinon.stub( tools, 'getCKE5Directories' );
+	it( 'should remove symlinks that are not needed', () => {
+		const dirs = [ 'ckeditor5-core', 'ckeditor5-devtest' ];
+		const installTask = sinon.spy();
+		spies.getDirectories = sinon.stub( tools, 'getCKE5Directories' ).returns( dirs );
+		spies.getCKE5Symlinks = sinon.stub( tools, 'getCKE5Symlinks' ).returns( [ 'ckeditor5-unused' ] );
+
 		const json = {
 		const json = {
 			dependencies: {
 			dependencies: {
+				'ckeditor5-core': 'ckeditor/ckeditor5-core',
+				'ckeditor5-devtest': 'ckeditor/ckeditor5-devtest#new-branch',
 				'other-plugin': '1.2.3'
 				'other-plugin': '1.2.3'
 			}
 			}
 		};
 		};
 
 
-		updateTask( ckeditor5Path, json, workspaceRoot, false );
+		updateTask( installTask, ckeditor5Path, json, workspaceRoot, true );
+
+		const repoPath1 = path.join( workspaceAbsolutePath, dirs[ 0 ] );
+		const repoPath2 = path.join( workspaceAbsolutePath, dirs[ 1 ] );
+
+		sinon.assert.calledTwice( spies.checkout );
+		sinon.assert.calledWithExactly( spies.checkout.firstCall, repoPath1, 'master' );
+		sinon.assert.calledWithExactly( spies.checkout.secondCall, repoPath2, 'new-branch' );
+		sinon.assert.calledTwice( spies.pull );
+		sinon.assert.calledWithExactly( spies.pull.firstCall, repoPath1, 'master' );
+		sinon.assert.calledWithExactly( spies.pull.secondCall, repoPath2, 'new-branch' );
+
+		sinon.assert.calledThrice( spies.npmUpdate );
+		sinon.assert.calledWithExactly( spies.npmUpdate.firstCall, path.join( workspaceAbsolutePath, dirs[ 0 ] ) );
+		sinon.assert.calledWithExactly( spies.npmUpdate.secondCall, path.join( workspaceAbsolutePath, dirs[ 1 ] ) );
+		sinon.assert.calledWithExactly( spies.npmUpdate.thirdCall, ckeditor5Path );
+
+		sinon.assert.calledOnce( spies.getCKE5Symlinks );
+		sinon.assert.notCalled( installTask );
+
+		sinon.assert.calledOnce( spies.removeSymlink );
+		sinon.assert.calledWithExactly( spies.removeSymlink, path.join( ckeditor5Path, 'node_modules', 'ckeditor5-unused' ) );
+	} );
+
+	it( 'should catch linking errors', () => {
+		const log = require( '../../tasks/dev/utils/log' );
+		const dirs = [ 'ckeditor5-core', 'ckeditor5-devtest' ];
+		const installTask = sinon.spy();
+		const outSpy = sinon.spy();
+		const errSpy = sinon.spy();
+		spies.getDirectories = sinon.stub( tools, 'getCKE5Directories' ).returns( dirs );
+		spies.getCKE5Symlinks = sinon.stub( tools, 'getCKE5Symlinks' ).returns( [ 'ckeditor5-unused' ] );
+		spies.linkDirectories.throws();
+
+		log.configure( outSpy, errSpy );
+
+		const json = {
+			dependencies: {
+				'ckeditor5-core': 'ckeditor/ckeditor5-core',
+				'ckeditor5-devtest': 'ckeditor/ckeditor5-devtest#new-branch',
+				'other-plugin': '1.2.3'
+			}
+		};
+
+		updateTask( installTask, ckeditor5Path, json, workspaceRoot, false );
+
+		const repoPath1 = path.join( workspaceAbsolutePath, dirs[ 0 ] );
+		const repoPath2 = path.join( workspaceAbsolutePath, dirs[ 1 ] );
+
+		sinon.assert.calledTwice( spies.checkout );
+		sinon.assert.calledWithExactly( spies.checkout.firstCall, repoPath1, 'master' );
+		sinon.assert.calledWithExactly( spies.checkout.secondCall, repoPath2, 'new-branch' );
+		sinon.assert.calledTwice( spies.pull );
+		sinon.assert.calledWithExactly( spies.pull.firstCall, repoPath1, 'master' );
+		sinon.assert.calledWithExactly( spies.pull.secondCall, repoPath2, 'new-branch' );
 
 
-		sinon.assert.notCalled( spies.pull );
 		sinon.assert.notCalled( spies.npmUpdate );
 		sinon.assert.notCalled( spies.npmUpdate );
+
+		sinon.assert.calledOnce( spies.getCKE5Symlinks );
+		sinon.assert.notCalled( installTask );
+
+		sinon.assert.calledOnce( spies.removeSymlink );
+		sinon.assert.calledWithExactly( spies.removeSymlink, path.join( ckeditor5Path, 'node_modules', 'ckeditor5-unused' ) );
+		sinon.assert.calledTwice( errSpy );
 	} );
 	} );
 
 
-	it( 'should not update when no plugins in dev mode', () => {
-		spies.getDirectories = sinon.stub( tools, 'getCKE5Directories' ).returns( [] );
+	it( 'should skip updating if no dependencies found', () => {
+		spies.getDependencies.restore();
+		spies.getDependencies = sinon.stub( tools, 'getCKEditorDependencies' ).returns( null );
+		const dirs = [ 'ckeditor5-core', 'ckeditor5-devtest' ];
+		const installTask = sinon.spy();
+		spies.getDirectories = sinon.stub( tools, 'getCKE5Directories' ).returns( dirs );
+		spies.getCKE5Symlinks = sinon.stub( tools, 'getCKE5Symlinks' ).returns( [] );
+
 		const json = {
 		const json = {
 			dependencies: {
 			dependencies: {
+				'ckeditor5-core': 'ckeditor/ckeditor5-core',
 				'ckeditor5-devtest': 'ckeditor/ckeditor5-devtest#new-branch',
 				'ckeditor5-devtest': 'ckeditor/ckeditor5-devtest#new-branch',
 				'other-plugin': '1.2.3'
 				'other-plugin': '1.2.3'
 			}
 			}
 		};
 		};
 
 
-		updateTask( ckeditor5Path, json, workspaceRoot, false );
+		updateTask( installTask, ckeditor5Path, json, workspaceRoot, false );
 
 
+		sinon.assert.notCalled( spies.checkout );
 		sinon.assert.notCalled( spies.pull );
 		sinon.assert.notCalled( spies.pull );
+
 		sinon.assert.notCalled( spies.npmUpdate );
 		sinon.assert.notCalled( spies.npmUpdate );
+
+		sinon.assert.calledOnce( spies.getCKE5Symlinks );
+		sinon.assert.notCalled( installTask );
+
+		sinon.assert.notCalled( spies.removeSymlink );
 	} );
 	} );
 } );
 } );

+ 1 - 1
package.json

@@ -9,9 +9,9 @@
   ],
   ],
   "dependencies": {
   "dependencies": {
     "ckeditor5-core": "ckeditor/ckeditor5-core",
     "ckeditor5-core": "ckeditor/ckeditor5-core",
-    "ckeditor5-utils": "ckeditor/ckeditor5-utils",
     "ckeditor5-ui": "ckeditor/ckeditor5-ui",
     "ckeditor5-ui": "ckeditor/ckeditor5-ui",
     "ckeditor5-ui-default": "ckeditor/ckeditor5-ui-default",
     "ckeditor5-ui-default": "ckeditor/ckeditor5-ui-default",
+    "ckeditor5-utils": "ckeditor/ckeditor5-utils",
     "requirejs": "Reinmar/requirejs"
     "requirejs": "Reinmar/requirejs"
   },
   },
   "devDependencies": {
   "devDependencies": {