ensure-integrity.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. /**
  2. * Ensure the integrity of the packages in the repo.
  3. *
  4. * Ensure the core package version dependencies match everywhere.
  5. * Ensure imports match dependencies for TypeScript packages.
  6. * Manage the all-packages meta package.
  7. */
  8. var childProcess = require('child_process');
  9. var path = require('path');
  10. var glob = require('glob');
  11. var sortPackageJson = require('sort-package-json');
  12. var ts = require("typescript");
  13. var fs = require('fs-extra');
  14. var getDependency = require('./get-dependency');
  15. // Data to ignore.
  16. var MISSING = {
  17. "@jupyterlab/buildutils": ["path"]
  18. }
  19. var UNUSED = {
  20. "@jupyterlab/apputils-extension": ["es6-promise"],
  21. "@jupyterlab/theme-dark-extension": ["font-awesome"],
  22. "@jupyterlab/theme-light-extension": ["font-awesome"],
  23. "@jupyterlab/vega2-extension": ["d3","vega","vega-lite"]
  24. }
  25. var pkgData = {};
  26. var pkgPaths = {};
  27. var pkgNames = {};
  28. var basePath = path.resolve('.');
  29. /**
  30. * Ensure the integrity of a package.
  31. */
  32. function ensurePackage(pkgName) {
  33. var dname = pkgPaths[pkgName];
  34. var data = pkgData[pkgName];
  35. var deps = data.dependencies;
  36. var messages = [];
  37. // Verify local dependencies are correct.
  38. Object.keys(deps).forEach(function(name) {
  39. if (pkgData[name]) {
  40. var desired = '^' + pkgData[name].version;
  41. if (deps[name] !== desired) {
  42. messages.push('Invalid core version: ' + name);
  43. }
  44. data.dependencies[name] = '^' + pkgData[name].version;
  45. }
  46. });
  47. if (pkgName == '@jupyterlab/all-packages') {
  48. messages = messages.concat(ensureAllPackages());
  49. }
  50. // For TypeScript files, verify imports match dependencies.
  51. filenames = glob.sync(path.join(dname, 'src/*.ts*'));
  52. filenames = filenames.concat(glob.sync(path.join(dname, 'src/**/*.ts*')));
  53. if (filenames.length == 0) {
  54. if (ensurePackageData(data, path.join(dname, 'package.json'))) {
  55. messages.push('Package data changed');
  56. }
  57. return messages;
  58. }
  59. var imports = [];
  60. // Extract all of the imports from the TypeScript files.
  61. filenames.forEach(fileName => {
  62. var sourceFile = ts.createSourceFile(fileName,
  63. fs.readFileSync(fileName).toString(), ts.ScriptTarget.ES6,
  64. /*setParentNodes */ true);
  65. imports = imports.concat(getImports(sourceFile));
  66. });
  67. var names = Array.from(new Set(imports)).sort();
  68. names = names.map(function(name) {
  69. var parts = name.split('/');
  70. if (name.indexOf('@') === 0) {
  71. return parts[0] + '/' + parts[1];
  72. }
  73. return parts[0];
  74. })
  75. // Look for imports with no dependencies.
  76. names.forEach(function(name) {
  77. if (MISSING[pkgName] && MISSING[pkgName].indexOf(name) !== -1) {
  78. return;
  79. }
  80. if (name == '.' || name == '..') {
  81. return;
  82. }
  83. if (!deps[name]) {
  84. messages.push('Missing dependency: ' + name);
  85. deps[name] = getDependency(name);
  86. }
  87. });
  88. // Look for unused packages
  89. Object.keys(deps).forEach(function(name) {
  90. if (UNUSED[pkgName] && UNUSED[pkgName].indexOf(name) !== -1) {
  91. return;
  92. }
  93. if (names.indexOf(name) === -1) {
  94. messages.push('Unused dependency: ' + name);
  95. delete data.dependencies[name]
  96. }
  97. });
  98. if (ensurePackageData(data, path.join(dname, 'package.json'))) {
  99. messages.push('Package data changed');
  100. }
  101. return messages;
  102. }
  103. /**
  104. * Ensure the all-packages package.
  105. */
  106. function ensureAllPackages() {
  107. var localPackages = glob.sync(path.join(basePath, 'packages', '*'));
  108. var allPackagesPath = path.join(basePath, 'packages', 'all-packages');
  109. var allPackageJson = path.join(allPackagesPath, 'package.json');
  110. var allPackageData = require(allPackageJson);
  111. var tsconfigPath = path.join(
  112. basePath, 'packages', 'all-packages', 'tsconfig.json'
  113. );
  114. var tsconfig = require(tsconfigPath);
  115. var indexPath = path.join(basePath, 'packages', 'all-packages', 'src', 'index.ts');
  116. var index = fs.readFileSync(indexPath, 'utf8');
  117. var lines = index.split('\n').slice(0, 3);
  118. var messages = [];
  119. localPackages.forEach(function (pkgPath) {
  120. if (pkgPath === allPackagesPath) {
  121. return;
  122. }
  123. var name = pkgNames[pkgPath];
  124. var data = pkgData[name];
  125. var valid = true;
  126. // Ensure it is a dependency.
  127. if (!allPackageData.dependencies[name]) {
  128. valid = false;
  129. allPackageData.dependencies[name] = '^' + data.version;
  130. }
  131. // Ensure it is in index.ts
  132. if (index.indexOf(name) === -1) {
  133. valid = false;
  134. }
  135. lines.push('import "' + name + '";\n');
  136. if (!valid) {
  137. messages.push('Updated: ' + name);
  138. }
  139. });
  140. // Write the files.
  141. if (ensurePackageData(allPackageData, allPackageJson)) {
  142. messages.push('Package data changed');
  143. }
  144. var newIndex = lines.join('\n');
  145. if (newIndex != index) {
  146. messages.push('Index changed');
  147. fs.writeFileSync(indexPath, lines.join('\n'));
  148. }
  149. return messages;
  150. }
  151. /**
  152. * Extract the module imports from a TypeScript source file.
  153. */
  154. function getImports(sourceFile) {
  155. var imports = [];
  156. handleNode(sourceFile);
  157. function handleNode(node) {
  158. switch (node.kind) {
  159. case ts.SyntaxKind.ImportDeclaration:
  160. imports.push(node.moduleSpecifier.text);
  161. break;
  162. case ts.SyntaxKind.ImportEqualsDeclaration:
  163. imports.push(node.moduleReference.expression.text);
  164. break;
  165. }
  166. ts.forEachChild(node, handleNode);
  167. }
  168. return imports;
  169. }
  170. /**
  171. * Write package data using sort-package-json.
  172. */
  173. function ensurePackageData(data, pkgJsonPath) {
  174. var text = JSON.stringify(sortPackageJson(data), null, 2) + '\n';
  175. var orig = fs.readFileSync(pkgJsonPath).toString();
  176. if (text !== orig) {
  177. fs.writeFileSync(pkgJsonPath, text);
  178. return true;
  179. }
  180. return false;
  181. }
  182. /**
  183. * Ensure the top level package.
  184. */
  185. function ensureTop() {
  186. // Hoist dependencies and devDependencies to top level.
  187. var localPath = path.join(basePath, 'package.json');
  188. var localData = require(localPath);
  189. var localPackages = glob.sync(path.join(basePath, 'packages', '*'));
  190. localPackages.forEach(function (pkgPath) {
  191. var name = pkgNames[pkgPath];
  192. var data = pkgData[name];
  193. var devDeps = data.dependencies || {};
  194. Object.keys(devDeps).forEach(function (name) {
  195. localData.devDependencies[name] = deps[name];
  196. });
  197. if (ensurePackageData(localData, localPath)) {
  198. return 'updated';
  199. }
  200. }
  201. }
  202. /**
  203. * Ensure the repo integrity.
  204. */
  205. function ensureIntegrity() {
  206. var messages = {};
  207. // Handle the top level package.
  208. var topMessage = ensureTop();
  209. if (topMessage) {
  210. messages['top'] = topMessage;
  211. }
  212. // Look in all of the packages.
  213. var lernaConfig = require(path.join(basePath, 'lerna.json'));
  214. var paths = [];
  215. for (let spec of lernaConfig.packages) {
  216. paths = paths.concat(glob.sync(path.join(basePath, spec)));
  217. }
  218. // Pick up all the package versions.
  219. paths.forEach(function(pkgPath) {
  220. pkgPath = path.resolve(pkgPath);
  221. // Read in the package.json.
  222. try {
  223. var package = require(path.join(pkgPath, 'package.json'));
  224. } catch (e) {
  225. return;
  226. }
  227. pkgData[package.name] = package;
  228. pkgPaths[package.name] = pkgPath;
  229. pkgNames[pkgPath] = package.name;
  230. });
  231. // Validate each package.
  232. for (let name in pkgData) {
  233. var pkgMessages = ensurePackage(name);
  234. if (pkgMessages.length > 0) {
  235. messages[name] = pkgMessages;
  236. }
  237. };
  238. // Handle any messages.
  239. if (Object.keys(messages).length > 0) {
  240. console.log(JSON.stringify(messages, null, 2));
  241. if (process.env.TRAVIS_BRANCH) {
  242. console.log('\n\nPlease run `npm run integrity` locally and commit the changes');
  243. } else {
  244. console.log('\n\nPlease commit the changes by running:');
  245. console.log('git commit -a -m "Package integrity updates"')
  246. }
  247. process.exit(1);
  248. } else {
  249. console.log('Repo integrity verified!');
  250. }
  251. }
  252. ensureIntegrity();