diff --git a/package.json b/package.json index f26adb3027e0..44f07ae84ff7 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,9 @@ "eslint": "eslint .", "tslint": "tslint \"**/*.ts\" -c tslint.json -e \"**/config/schema.d.ts\" -e \"**/tests/**\" -e \"**/blueprints/*/files/**/*.ts\" -e \"node_modules/**\" -e \"tmp/**\" -e \"dist/**\"", "lint": "npm-run-all -c eslint tslint", - "tool": "node scripts/run-tool.js" + "tool": "node scripts/run-tool.js", + "sync-deps": "node ./scripts/sync-deps.js", + "precommit": "npm run sync-deps" }, "repository": { "type": "git", @@ -54,7 +56,7 @@ "exports-loader": "^0.6.3", "extract-text-webpack-plugin": "^2.1.0", "file-loader": "^0.10.0", - "fs-extra": "~2.0.0", + "fs-extra": "^2.0.0", "get-caller-file": "^1.0.0", "glob": "^7.0.3", "html-webpack-plugin": "^2.19.0", @@ -92,7 +94,7 @@ "stylus": "^0.54.5", "stylus-loader": "^3.0.1", "temp": "0.8.3", - "typescript": "~2.2.0", + "typescript": ">=2.0.0 <2.3.0", "url-loader": "^0.5.7", "walk-sync": "^0.3.1", "webpack": "~2.2.0", @@ -132,6 +134,7 @@ "eslint": "^3.11.0", "exists-sync": "0.0.4", "express": "^4.14.0", + "husky": "^0.13.3", "jasmine": "^2.4.1", "jasmine-spec-reporter": "^3.2.0", "minimist": "^1.2.0", diff --git a/packages/@angular/cli/package.json b/packages/@angular/cli/package.json index 629930c59ef4..f51246f36c6d 100644 --- a/packages/@angular/cli/package.json +++ b/packages/@angular/cli/package.json @@ -69,7 +69,7 @@ "rxjs": "^5.0.1", "sass-loader": "^6.0.3", "script-loader": "^0.7.0", - "semver": "^5.1.0", + "semver": "^5.3.0", "silent-error": "^1.0.0", "source-map-loader": "^0.1.5", "istanbul-instrumenter-loader": "^2.0.0", diff --git a/scripts/publish/validate_dependencies.js b/scripts/publish/validate_dependencies.js index eb7abda45224..bfe129b0c31c 100644 --- a/scripts/publish/validate_dependencies.js +++ b/scripts/publish/validate_dependencies.js @@ -7,6 +7,7 @@ const fs = require('fs'); const glob = require('glob'); const packages = require('../../lib/packages').packages; const path = require('path'); +const _ = require('lodash'); const IMPORT_RE = /(^|\n)\s*import\b(?:.|\n)*?\'[^\']*\'/g; @@ -32,6 +33,18 @@ const OPTIONAL_PACKAGES = [ '@angular/service-worker' ]; +let exitCode = 0; + +const depsInfo = validateDependenciesPerPackage(packages); + +validateRootPackageJson(depsInfo.allSourceDeps); + +checkDependenciesConflictAcrossPackages(depsInfo.allDeclaredDepsMap); + +process.exit(exitCode); + + +// utils function listImportedModules(source) { const imports = source.match(IMPORT_RE); @@ -42,9 +55,9 @@ function listImportedModules(source) { return m && m[1]; }) .filter(x => !!x) - .filter(modulePath => modulePath[0] != '.') + .filter(modulePath => modulePath[0] !== '.') .map(fullImportPath => { - if (fullImportPath[0] == '@') { + if (fullImportPath[0] === '@') { // Need to get the scope as well. return fullImportPath.split('/').slice(0, 2).join('/'); } else { @@ -62,9 +75,9 @@ function listRequiredModules(source) { return m && m[1]; }) .filter(x => !!x) - .filter(modulePath => modulePath[0] != '.') + .filter(modulePath => modulePath[0] !== '.') .map(fullImportPath => { - if (fullImportPath[0] == '@') { + if (fullImportPath[0] === '@') { // Need to get the scope as well. return fullImportPath.split('/').slice(0, 2).join('/'); } else { @@ -84,7 +97,7 @@ function listIgnoredModules(source) { } function reportMissingDependencies(missingDeps) { - if (missingDeps.length == 0) { + if (missingDeps.length === 0) { console.log(chalk.green(' no dependency missing from package.json.')); } else { console.log(chalk.yellow(` ${missingDeps.length} missing from package.json:`)); @@ -94,7 +107,7 @@ function reportMissingDependencies(missingDeps) { } function reportExcessiveDependencies(overDeps) { - if (overDeps.length == 0) { + if (overDeps.length === 0) { console.log(chalk.green(' no excessive dependencies in package.json.')); } else { console.log(chalk.yellow(` ${overDeps.length} excessive dependencies in package.json:`)); @@ -103,18 +116,23 @@ function reportExcessiveDependencies(overDeps) { } } -let exitCode = 0; -const overallDeps = []; -for (const packageName of Object.keys(packages)) { - console.log(chalk.green(`Reading dependencies of "${packageName}".`)); +function reportInconsistentDepsDependencies(inconsistentDeps) { + if (inconsistentDeps.length === 0) { + console.log(chalk.green(' no inconsistentDeps dependencies across packages.')); + } else { + console.log(chalk.yellow(` ${inconsistentDeps.length} inconsistent dependencies found:`)); + inconsistentDeps.forEach(arr => console.log(` ${arr.join(' vs ')}`)); + exitCode = 0; + } +} +function retrieveDependencyMapFromPackageSource(packageName) { const allSources = glob.sync(path.join(__dirname, '../../packages/', packageName, '**/*')) .filter(p => p.match(/\.(js|ts)$/)) .filter(p => !p.match(/\.spec\./)) .filter(p => !p.match(/\/blueprints\//)); - const importMap = {}; - allSources.forEach(function(filePath) { + return allSources.reduce(function (importMap, filePath) { const source = fs.readFileSync(filePath, 'utf8'); listImportedModules(source) @@ -125,50 +143,95 @@ for (const packageName of Object.keys(packages)) { .forEach(modulePath => { delete importMap[modulePath]; }); - }); - - const dependencies = Object.keys(importMap) - // Filter out the node packages that should not be depended on. - .filter(x => NODE_PACKAGES.indexOf(x) == -1); - overallDeps.push(...dependencies); - - console.log(chalk.green(` found ${dependencies.length} dependencies...`)); - const packageJson = JSON.parse(fs.readFileSync(packages[packageName].packageJson, 'utf8')); - const allDeps = [] - .concat(Object.keys(packageJson['dependencies'] || {})) - .concat(Object.keys(packageJson['optionalDependencies'] || {})) - .concat(Object.keys(packageJson['devDependencies'] || {})) - .concat(Object.keys(packageJson['peerDependencies'] || {})); - - const missingDeps = dependencies - .filter(d => allDeps.indexOf(d) == -1) - .filter(d => OPTIONAL_PACKAGES.indexOf(d) == -1); - reportMissingDependencies(missingDeps); - - const overDeps = allDeps.filter(d => dependencies.indexOf(d) == -1) - .filter(x => ANGULAR_PACKAGES.indexOf(x) == -1); - reportExcessiveDependencies(overDeps); - - console.log(''); + + return importMap; + }, {}); +} + +function validateDependenciesPerPackage(packages) { + const allSourceDeps = []; + const allDeclaredDepsMap = {}; + + for (const packageName of Object.keys(packages)) { + console.log(chalk.blue(`Reading dependencies of "${packageName}".`)); + + const importMap = retrieveDependencyMapFromPackageSource(packageName) + + const dependencies = Object.keys(importMap) + // Filter out the node packages that should not be depended on. + .filter(x => NODE_PACKAGES.indexOf(x) === -1); + allSourceDeps.push(...dependencies); + + console.log(chalk.green(` found ${dependencies.length} dependencies...`)); + + const packageJson = JSON.parse(fs.readFileSync(packages[packageName].packageJson, 'utf8')); + + const DeclaredDepsMap = Object.assign( + {}, + packageJson['dependencies'] || {}, + packageJson['optionalDependencies'] || {}, + packageJson['devDependencies'] || {}, + packageJson['peerDependencies'] || {} + ); + allDeclaredDepsMap[packageName] = DeclaredDepsMap; + const declaredDeps = Object.keys(DeclaredDepsMap); + + const missingDeps = dependencies.filter(d => declaredDeps.indexOf(d) === -1) + .filter(d => OPTIONAL_PACKAGES.indexOf(d) === -1); + reportMissingDependencies(missingDeps); + + const excessiveDeps = declaredDeps.filter(d => dependencies.indexOf(d) === -1) + .filter(x => ANGULAR_PACKAGES.indexOf(x) === -1); + reportExcessiveDependencies(excessiveDeps); + } + + return { + allDeclaredDepsMap, + allSourceDeps + }; } -console.log(chalk.green('Validating root package. [devDependencies ignored]')); -const rootPackagePath = path.join(__dirname, '../../package.json'); -const rootPackageJson = JSON.parse(fs.readFileSync(rootPackagePath, 'utf8')); -// devDependencies are ignored -const allRootDeps = [] +function validateRootPackageJson(overallDeps) { + console.log(chalk.blue('Validating root package. [devDependencies ignored]')); + + const rootPackagePath = path.join(__dirname, '../../package.json'); + const rootPackageJson = JSON.parse(fs.readFileSync(rootPackagePath, 'utf8')); + + // devDependencies are ignored + const allRootDeps = [] .concat(Object.keys(rootPackageJson['dependencies'] || {})) .concat(Object.keys(rootPackageJson['optionalDependencies'] || {})) .concat(Object.keys(rootPackageJson['peerDependencies'] || {})); -const internalPackages = Object.keys(packages); -const missingRootDeps = overallDeps.filter(d => allRootDeps.indexOf(d) == -1) - .filter(d => internalPackages.indexOf(d) == -1) - .filter(x => OPTIONAL_PACKAGES.indexOf(x) == -1); -reportMissingDependencies(missingRootDeps); + const internalPackages = Object.keys(packages); + const missingRootDeps = overallDeps.filter(d => allRootDeps.indexOf(d) === -1) + .filter(d => internalPackages.indexOf(d) === -1) + .filter(x => OPTIONAL_PACKAGES.indexOf(x) === -1); + reportMissingDependencies(missingRootDeps); + + const overRootDeps = allRootDeps.filter(d => overallDeps.indexOf(d) === -1) + .filter(x => ANGULAR_PACKAGES.indexOf(x) === -1); + reportExcessiveDependencies(overRootDeps); +} -const overRootDeps = allRootDeps.filter(d => overallDeps.indexOf(d) == -1) - .filter(x => ANGULAR_PACKAGES.indexOf(x) == -1); -reportExcessiveDependencies(overRootDeps); +function checkDependenciesConflictAcrossPackages(allDeclaredDepsMap) { + console.log(chalk.blue('Validating packages\' dependencies conflict.')); -process.exit(exitCode); + const allDeclaredDeps = Object.keys(allDeclaredDepsMap) + .reduce((collect, packageName) => collect.concat(Object.keys(allDeclaredDepsMap[packageName])), []); + + const inconsistentDeps = _.uniq(allDeclaredDeps).reduce(function (collect, dep) { + const test = _(allDeclaredDepsMap) + .map((map, name) => map[dep] ? [name, `${dep}@${map[dep]}`] : null) + .compact() + .uniqBy(1) + .value(); + if (test.length > 1) { + collect.push(test.map(pair =>`[${pair[0]}]: ${pair[1]}`)); + } + + return collect; + }, []); + + reportInconsistentDepsDependencies(inconsistentDeps); +} diff --git a/scripts/sync-deps.js b/scripts/sync-deps.js new file mode 100644 index 000000000000..f1127dd44dd2 --- /dev/null +++ b/scripts/sync-deps.js @@ -0,0 +1,64 @@ +const denodeify = require('denodeify'); +const glob = denodeify(require('glob')); +const fs = require('fs'); +const path = require('path'); +const _ = require('lodash'); +const readFile = denodeify(fs.readFile); +const writeFile = denodeify(fs.writeFile); + +const root = path.join(__dirname, '../'); +const packagesRoot = path.join(root, 'packages'); + +function readJson(file) { + return readFile(file) + .then((content) => { + let obj + try { + obj = JSON.parse(content.toString('utf8')) + } catch (err) { + throw err + } + return obj + }) +} + +function sortObjectKeys(obj) { + const sortedKeys = Object.keys(obj).sort() + return sortedKeys.reduce(function (newObj, key) { + newObj[key] = obj[key] + return newObj + }, {}) +} + +Promise.resolve() + .then(() => glob(path.join(packagesRoot, '**/package.json'))) + .then((files) => files.filter(p => !p.match(/\/blueprints\//))) + .then((files) => { + return files.reduce((prev, filename) => + prev.then((sum) => + readJson(filename) + .then((obj) => { + sum.push(obj); + return Promise.resolve(sum); + }) + ), Promise.resolve([])) + }) + .then((jsonArr) => { + const dependencies = _.merge(..._.map(jsonArr, 'dependencies')); + const peerDependencies = _.merge(..._.map(jsonArr, 'peerDependencies')); + + const rootPackageJsonPath = path.join(root, 'package.json'); + + const ignoredDeps = jsonArr.map(conf => conf.name); + + ignoredDeps.forEach(key => delete dependencies[key]) + + return readJson(rootPackageJsonPath) + .then((rootPackageJson) => { + rootPackageJson.dependencies = sortObjectKeys(Object.assign({}, peerDependencies, dependencies)); + + Object.keys(dependencies).forEach(key => delete rootPackageJson.devDependencies[key]); + + return writeFile(rootPackageJsonPath, JSON.stringify(rootPackageJson, null, 2) + '\n'); + }) + })