ensure-repo.ts 14 KB

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