ensure-repo.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. /* -----------------------------------------------------------------------------
  2. | Copyright (c) Jupyter Development Team.
  3. | Distributed under the terms of the Modified BSD License.
  4. |----------------------------------------------------------------------------*/
  5. /**
  6. * Ensure the integrity of the packages in the repo.
  7. *
  8. * Ensure the core package version dependencies match everywhere.
  9. * Ensure imported packages match dependencies.
  10. * Ensure a consistent version of all packages.
  11. * Manage the metapackage meta package.
  12. */
  13. import * as path from 'path';
  14. import * as utils from './utils';
  15. import {
  16. ensurePackage,
  17. ensureUiComponents,
  18. IEnsurePackageOptions
  19. } from './ensure-package';
  20. type Dict<T> = { [key: string]: T };
  21. // Data to ignore.
  22. const MISSING: Dict<string[]> = {
  23. '@jupyterlab/buildutils': ['path'],
  24. '@jupyterlab/testutils': ['fs'],
  25. '@jupyterlab/vega5-extension': ['vega-embed']
  26. };
  27. const UNUSED: Dict<string[]> = {
  28. // url is a polyfill for sanitize-html
  29. '@jupyterlab/apputils': ['@types/react', 'htmlparser2', 'url'],
  30. '@jupyterlab/application': ['@fortawesome/fontawesome-free'],
  31. '@jupyterlab/apputils-extension': ['es6-promise'],
  32. '@jupyterlab/services': ['node-fetch', 'ws'],
  33. '@jupyterlab/rendermime': ['@jupyterlab/mathjax2'],
  34. '@jupyterlab/testutils': [
  35. 'node-fetch',
  36. 'identity-obj-proxy',
  37. 'jest-raw-loader',
  38. 'markdown-loader-jest',
  39. 'jest-junit',
  40. 'jest-summary-reporter'
  41. ],
  42. '@jupyterlab/test-csvviewer': ['csv-spectrum'],
  43. '@jupyterlab/vega5-extension': ['vega', 'vega-lite'],
  44. '@jupyterlab/ui-components': ['@blueprintjs/icons']
  45. };
  46. // Packages that are allowed to have differing versions
  47. const DIFFERENT_VERSIONS: Array<string> = ['vega-lite', 'vega', 'vega-embed'];
  48. const SKIP_CSS: Dict<string[]> = {
  49. '@jupyterlab/application': ['@jupyterlab/rendermime'],
  50. '@jupyterlab/application-extension': ['@jupyterlab/apputils'],
  51. '@jupyterlab/completer': ['@jupyterlab/codeeditor'],
  52. '@jupyterlab/docmanager': ['@jupyterlab/statusbar'], // Statusbar styles should not be used by status reporters
  53. '@jupyterlab/docregistry': [
  54. '@jupyterlab/codeeditor', // Only used for model
  55. '@jupyterlab/codemirror', // Only used for Mode.findByFileName
  56. '@jupyterlab/rendermime' // Only used for model
  57. ],
  58. '@jupyterlab/documentsearch': [
  59. '@jupyterlab/cells',
  60. '@jupyterlab/codeeditor',
  61. '@jupyterlab/codemirror',
  62. '@jupyterlab/fileeditor',
  63. '@jupyterlab/notebook'
  64. ],
  65. '@jupyterlab/filebrowser': ['@jupyterlab/statusbar'],
  66. '@jupyterlab/fileeditor': ['@jupyterlab/statusbar'],
  67. '@jupyterlab/help-extension': ['@jupyterlab/application'],
  68. '@jupyterlab/shortcuts-extension': ['@jupyterlab/application'],
  69. '@jupyterlab/tabmanager-extension': ['@jupyterlab/application'],
  70. '@jupyterlab/theme-dark-extension': [
  71. '@jupyterlab/application',
  72. '@jupyterlab/apputils'
  73. ],
  74. '@jupyterlab/theme-light-extension': [
  75. '@jupyterlab/application',
  76. '@jupyterlab/apputils'
  77. ],
  78. '@jupyterlab/ui-extension': ['@blueprintjs/icons']
  79. };
  80. const pkgData: Dict<any> = {};
  81. const pkgPaths: Dict<string> = {};
  82. const pkgNames: Dict<string> = {};
  83. const depCache: Dict<string> = {};
  84. const locals: Dict<string> = {};
  85. /**
  86. * Ensure the metapackage package.
  87. *
  88. * @returns An array of messages for changes.
  89. */
  90. function ensureMetaPackage(): string[] {
  91. const basePath = path.resolve('.');
  92. const mpPath = path.join(basePath, 'packages', 'metapackage');
  93. const mpJson = path.join(mpPath, 'package.json');
  94. const mpData = utils.readJSONFile(mpJson);
  95. const messages: string[] = [];
  96. const seen: Dict<boolean> = {};
  97. utils.getCorePaths().forEach(pkgPath => {
  98. if (path.resolve(pkgPath) === path.resolve(mpPath)) {
  99. return;
  100. }
  101. const name = pkgNames[pkgPath];
  102. if (!name) {
  103. return;
  104. }
  105. seen[name] = true;
  106. const data = pkgData[name];
  107. let valid = true;
  108. // Ensure it is a dependency.
  109. if (!mpData.dependencies[name]) {
  110. valid = false;
  111. mpData.dependencies[name] = '^' + data.version;
  112. }
  113. if (!valid) {
  114. messages.push(`Updated: ${name}`);
  115. }
  116. });
  117. // Make sure there are no extra deps.
  118. Object.keys(mpData.dependencies).forEach(name => {
  119. if (!(name in seen)) {
  120. messages.push(`Removing dependency: ${name}`);
  121. delete mpData.dependencies[name];
  122. }
  123. });
  124. // Write the files.
  125. if (messages.length > 0) {
  126. utils.writePackageData(mpJson, mpData);
  127. }
  128. // Update the global data.
  129. pkgData[mpData.name] = mpData;
  130. return messages;
  131. }
  132. /**
  133. * Ensure the jupyterlab application package.
  134. */
  135. function ensureJupyterlab(): string[] {
  136. const basePath = path.resolve('.');
  137. const corePath = path.join(basePath, 'dev_mode', 'package.json');
  138. const corePackage = utils.readJSONFile(corePath);
  139. corePackage.jupyterlab.extensions = {};
  140. corePackage.jupyterlab.mimeExtensions = {};
  141. corePackage.jupyterlab.linkedPackages = {};
  142. // start with known external dependencies
  143. corePackage.dependencies = Object.assign(
  144. {},
  145. corePackage.jupyterlab.externalExtensions
  146. );
  147. corePackage.resolutions = {};
  148. const singletonPackages: string[] = corePackage.jupyterlab.singletonPackages;
  149. const coreData = new Map<string, any>();
  150. utils.getCorePaths().forEach(pkgPath => {
  151. const dataPath = path.join(pkgPath, 'package.json');
  152. let data: any;
  153. try {
  154. data = utils.readJSONFile(dataPath);
  155. } catch (e) {
  156. return;
  157. }
  158. coreData.set(data.name, data);
  159. });
  160. // Populate the yarn resolutions. First we make sure direct packages have
  161. // resolutions.
  162. coreData.forEach((data, name) => {
  163. // Insist on a restricted version in the yarn resolution.
  164. corePackage.resolutions[name] = `~${data.version}`;
  165. });
  166. // Then fill in any missing packages that should be singletons from the direct
  167. // package dependencies.
  168. coreData.forEach(data => {
  169. if (data.dependencies) {
  170. Object.entries(data.dependencies).forEach(([dep, version]) => {
  171. if (
  172. singletonPackages.includes(dep) &&
  173. !(dep in corePackage.resolutions)
  174. ) {
  175. corePackage.resolutions[dep] = version;
  176. }
  177. });
  178. }
  179. });
  180. // At this point, each singleton should have a resolution. Check this.
  181. const unresolvedSingletons = singletonPackages.filter(
  182. pkg => !(pkg in corePackage.resolutions)
  183. );
  184. if (unresolvedSingletons.length > 0) {
  185. throw new Error(
  186. `Singleton packages must have a resolved version number; these do not: ${unresolvedSingletons.join(
  187. ', '
  188. )}`
  189. );
  190. }
  191. coreData.forEach((data, name) => {
  192. // Determine if the package wishes to be included in the top-level
  193. // dependencies.
  194. const meta = data.jupyterlab;
  195. const keep = !!(
  196. meta &&
  197. (meta.coreDependency || meta.extension || meta.mimeExtension)
  198. );
  199. if (!keep) {
  200. return;
  201. }
  202. // Make sure it is included as a dependency.
  203. corePackage.dependencies[data.name] = `~${data.version}`;
  204. // Handle extensions.
  205. ['extension', 'mimeExtension'].forEach(item => {
  206. let ext = meta[item];
  207. if (ext === true) {
  208. ext = '';
  209. }
  210. if (typeof ext !== 'string') {
  211. return;
  212. }
  213. corePackage.jupyterlab[`${item}s`][name] = ext;
  214. });
  215. });
  216. utils.getLernaPaths().forEach(pkgPath => {
  217. const dataPath = path.join(pkgPath, 'package.json');
  218. let data: any;
  219. try {
  220. data = utils.readJSONFile(dataPath);
  221. } catch (e) {
  222. return;
  223. }
  224. // Skip private packages.
  225. if (data.private === true) {
  226. return;
  227. }
  228. // watch all src, build, and test files in the Jupyterlab project
  229. const relativePath = utils.ensureUnixPathSep(
  230. path.join('..', path.relative(basePath, pkgPath))
  231. );
  232. corePackage.jupyterlab.linkedPackages[data.name] = relativePath;
  233. });
  234. // Write the package.json back to disk.
  235. if (utils.writePackageData(corePath, corePackage)) {
  236. return ['Updated dev mode'];
  237. }
  238. return [];
  239. }
  240. /**
  241. * Ensure the repo integrity.
  242. */
  243. export async function ensureIntegrity(): Promise<boolean> {
  244. const messages: Dict<string[]> = {};
  245. // Pick up all the package versions.
  246. const paths = utils.getLernaPaths();
  247. // These two are not part of the workspaces but should be kept
  248. // in sync.
  249. paths.push('./jupyterlab/tests/mock_packages/extension');
  250. paths.push('./jupyterlab/tests/mock_packages/mimeextension');
  251. const cssImports: Dict<Array<string>> = {};
  252. // Get the package graph.
  253. const graph = utils.getPackageGraph();
  254. // Gather all of our package data and other metadata.
  255. paths.forEach(pkgPath => {
  256. // Read in the package.json.
  257. let data: any;
  258. try {
  259. data = utils.readJSONFile(path.join(pkgPath, 'package.json'));
  260. } catch (e) {
  261. console.error(e);
  262. return;
  263. }
  264. pkgData[data.name] = data;
  265. pkgPaths[data.name] = pkgPath;
  266. pkgNames[pkgPath] = data.name;
  267. locals[data.name] = pkgPath;
  268. });
  269. // Build up an ordered list of CSS imports for each local package.
  270. Object.keys(locals).forEach(name => {
  271. const data = pkgData[name];
  272. const deps: Dict<string> = data.dependencies || {};
  273. const skip = SKIP_CSS[name] || [];
  274. const cssData: Dict<Array<string>> = {};
  275. if (data.jupyterlab && data.jupyterlab.extraStyles) {
  276. Object.keys(data.jupyterlab.extraStyles).forEach(depName => {
  277. cssData[depName] = data.jupyterlab.extraStyles[depName];
  278. });
  279. }
  280. Object.keys(deps).forEach(depName => {
  281. // Bail for skipped imports and known extra styles.
  282. if (skip.indexOf(depName) !== -1 || depName in cssData) {
  283. return;
  284. }
  285. const depData = graph.getNodeData(depName);
  286. if (typeof depData.style === 'string') {
  287. cssData[depName] = [depData.style];
  288. }
  289. });
  290. // Get our CSS imports in dependency order.
  291. cssImports[name] = [];
  292. graph.dependenciesOf(name).forEach(depName => {
  293. if (depName in cssData) {
  294. cssData[depName].forEach(cssPath => {
  295. cssImports[name].push(`${depName}/${cssPath}`);
  296. });
  297. }
  298. });
  299. });
  300. // Update the metapackage.
  301. let pkgMessages = ensureMetaPackage();
  302. if (pkgMessages.length > 0) {
  303. const pkgName = '@jupyterlab/metapackage';
  304. if (!messages[pkgName]) {
  305. messages[pkgName] = [];
  306. }
  307. messages[pkgName] = messages[pkgName].concat(pkgMessages);
  308. }
  309. // Validate each package.
  310. for (const name in locals) {
  311. // application-top is handled elsewhere
  312. if (name === '@jupyterlab/application-top') {
  313. continue;
  314. }
  315. const unused = UNUSED[name] || [];
  316. // Allow jest-junit to be unused in the test suite.
  317. if (name.indexOf('@jupyterlab/test-') === 0) {
  318. unused.push('jest-junit');
  319. }
  320. const options: IEnsurePackageOptions = {
  321. pkgPath: pkgPaths[name],
  322. data: pkgData[name],
  323. depCache,
  324. missing: MISSING[name],
  325. unused,
  326. locals,
  327. cssImports: cssImports[name],
  328. differentVersions: DIFFERENT_VERSIONS
  329. };
  330. if (name === '@jupyterlab/metapackage') {
  331. options.noUnused = false;
  332. }
  333. const pkgMessages = await ensurePackage(options);
  334. if (pkgMessages.length > 0) {
  335. messages[name] = pkgMessages;
  336. }
  337. }
  338. // ensure the icon svg imports
  339. pkgMessages = await ensureUiComponents(pkgPaths['@jupyterlab/ui-components']);
  340. if (pkgMessages.length > 0) {
  341. const pkgName = '@jupyterlab/ui-components';
  342. if (!messages[pkgName]) {
  343. messages[pkgName] = [];
  344. }
  345. messages[pkgName] = messages[pkgName].concat(pkgMessages);
  346. }
  347. // Handle the top level package.
  348. const corePath = path.resolve('.', 'package.json');
  349. const coreData: any = utils.readJSONFile(corePath);
  350. if (utils.writePackageData(corePath, coreData)) {
  351. messages['top'] = ['Update package.json'];
  352. }
  353. // Handle the refs in the top level tsconfigdoc.json
  354. const tsConfigdocPath = path.resolve('.', 'tsconfigdoc.json');
  355. const tsConfigdocData = utils.readJSONFile(tsConfigdocPath);
  356. tsConfigdocData.references = [];
  357. utils.getCorePaths().forEach(pth => {
  358. tsConfigdocData.references.push({ path: './' + path.relative('.', pth) });
  359. });
  360. utils.writeJSONFile(tsConfigdocPath, tsConfigdocData);
  361. // Handle the JupyterLab application top package.
  362. pkgMessages = ensureJupyterlab();
  363. if (pkgMessages.length > 0) {
  364. messages['@application/top'] = pkgMessages;
  365. }
  366. // Handle any messages.
  367. if (Object.keys(messages).length > 0) {
  368. console.debug(JSON.stringify(messages, null, 2));
  369. if (process.argv.indexOf('--force') !== -1) {
  370. console.debug(
  371. '\n\nPlease run `jlpm run integrity` locally and commit the changes'
  372. );
  373. process.exit(1);
  374. }
  375. utils.run('jlpm install');
  376. console.debug('\n\nMade integrity changes!');
  377. console.debug('Please commit the changes by running:');
  378. console.debug('git commit -a -m "Package integrity updates"');
  379. return false;
  380. }
  381. console.debug('Repo integrity verified!');
  382. return true;
  383. }
  384. if (require.main === module) {
  385. void ensureIntegrity();
  386. }