ensure-package.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. /*-----------------------------------------------------------------------------
  2. | Copyright (c) Jupyter Development Team.
  3. | Distributed under the terms of the Modified BSD License.
  4. |----------------------------------------------------------------------------*/
  5. import * as fs from 'fs-extra';
  6. import * as glob from 'glob';
  7. import * as path from 'path';
  8. import * as ts from 'typescript';
  9. import { getDependency } from './get-dependency';
  10. import * as utils from './utils';
  11. const HEADER_TEMPLATE = `
  12. /*-----------------------------------------------------------------------------
  13. | Copyright (c) Jupyter Development Team.
  14. | Distributed under the terms of the Modified BSD License.
  15. |----------------------------------------------------------------------------*/
  16. /* This file was auto-generated by {{funcName}}() in @jupyterlab/buildutils */
  17. `;
  18. function generatedHeader(funcName: string): string {
  19. return HEADER_TEMPLATE.split('{{funcName}}')
  20. .join(funcName)
  21. .trim();
  22. }
  23. /**
  24. * Ensure the integrity of a package.
  25. *
  26. * @param options - The options used to ensure the package.
  27. *
  28. * @returns A list of changes that were made to ensure the package.
  29. */
  30. export async function ensurePackage(
  31. options: IEnsurePackageOptions
  32. ): Promise<string[]> {
  33. let { data, pkgPath } = options;
  34. let deps: { [key: string]: string } = data.dependencies || {};
  35. let devDeps: { [key: string]: string } = data.devDependencies || {};
  36. let seenDeps = options.depCache || {};
  37. let missing = options.missing || [];
  38. let unused = options.unused || [];
  39. let messages: string[] = [];
  40. let locals = options.locals || {};
  41. let cssImports = options.cssImports || [];
  42. let differentVersions = options.differentVersions || [];
  43. // Verify dependencies are consistent.
  44. let promises = Object.keys(deps).map(async name => {
  45. if (differentVersions.indexOf(name) !== -1) {
  46. // Skip processing packages that can have different versions
  47. return;
  48. }
  49. if (!(name in seenDeps)) {
  50. seenDeps[name] = await getDependency(name);
  51. }
  52. if (deps[name] !== seenDeps[name]) {
  53. messages.push(`Updated dependency: ${name}@${seenDeps[name]}`);
  54. }
  55. deps[name] = seenDeps[name];
  56. });
  57. await Promise.all(promises);
  58. // Verify devDependencies are consistent.
  59. promises = Object.keys(devDeps).map(async name => {
  60. if (differentVersions.indexOf(name) !== -1) {
  61. // Skip processing packages that can have different versions
  62. return;
  63. }
  64. if (!(name in seenDeps)) {
  65. seenDeps[name] = await getDependency(name);
  66. }
  67. if (devDeps[name] !== seenDeps[name]) {
  68. messages.push(`Updated devDependency: ${name}@${seenDeps[name]}`);
  69. }
  70. devDeps[name] = seenDeps[name];
  71. });
  72. await Promise.all(promises);
  73. // For TypeScript files, verify imports match dependencies.
  74. let filenames: string[] = [];
  75. filenames = glob.sync(path.join(pkgPath, 'src/*.ts*'));
  76. filenames = filenames.concat(glob.sync(path.join(pkgPath, 'src/**/*.ts*')));
  77. if (!fs.existsSync(path.join(pkgPath, 'tsconfig.json'))) {
  78. if (utils.writePackageData(path.join(pkgPath, 'package.json'), data)) {
  79. messages.push('Updated package.json');
  80. }
  81. return messages;
  82. }
  83. let imports: string[] = [];
  84. // Extract all of the imports from the TypeScript files.
  85. filenames.forEach(fileName => {
  86. let sourceFile = ts.createSourceFile(
  87. fileName,
  88. fs.readFileSync(fileName).toString(),
  89. (ts.ScriptTarget as any).ES6,
  90. /*setParentNodes */ true
  91. );
  92. imports = imports.concat(getImports(sourceFile));
  93. });
  94. // Make sure we are not importing CSS in a core package.
  95. if (data.name.indexOf('example') === -1) {
  96. imports.forEach(importStr => {
  97. if (importStr.indexOf('.css') !== -1) {
  98. messages.push('CSS imports are not allowed source files');
  99. }
  100. });
  101. }
  102. let names: string[] = Array.from(new Set(imports)).sort();
  103. names = names.map(function(name) {
  104. let parts = name.split('/');
  105. if (name.indexOf('@') === 0) {
  106. return parts[0] + '/' + parts[1];
  107. }
  108. return parts[0];
  109. });
  110. // Look for imports with no dependencies.
  111. promises = names.map(async name => {
  112. if (missing.indexOf(name) !== -1) {
  113. return;
  114. }
  115. if (name === '.' || name === '..') {
  116. return;
  117. }
  118. if (!deps[name]) {
  119. if (!(name in seenDeps)) {
  120. seenDeps[name] = await getDependency(name);
  121. }
  122. deps[name] = seenDeps[name];
  123. messages.push(`Added dependency: ${name}@${seenDeps[name]}`);
  124. }
  125. });
  126. await Promise.all(promises);
  127. // Template the CSS index file.
  128. if (cssImports && fs.existsSync(path.join(pkgPath, 'style/base.css'))) {
  129. let cssIndex = generatedHeader('ensurePackage');
  130. cssImports.forEach(cssImport => {
  131. cssIndex += `\n@import url('~${cssImport}');`;
  132. });
  133. cssIndex += "\n\n@import url('./base.css');\n";
  134. const cssPath = path.join(pkgPath, 'style/index.css');
  135. const prev = fs.readFileSync(cssPath, { encoding: 'utf8' });
  136. if (prev !== cssIndex) {
  137. messages.push(`Updated ./${data.style}`);
  138. fs.writeFileSync(cssPath, cssIndex);
  139. }
  140. }
  141. // Look for unused packages
  142. Object.keys(deps).forEach(name => {
  143. if (options.noUnused === false) {
  144. return;
  145. }
  146. if (unused.indexOf(name) !== -1) {
  147. return;
  148. }
  149. const isTest = data.name.indexOf('test') !== -1;
  150. if (isTest) {
  151. const testLibs = ['jest', 'ts-jest', '@jupyterlab/testutils'];
  152. if (testLibs.indexOf(name) !== -1) {
  153. return;
  154. }
  155. }
  156. if (names.indexOf(name) === -1) {
  157. let version = data.dependencies[name];
  158. messages.push(
  159. `Unused dependency: ${name}@${version}: remove or add to list of known unused dependencies for this package`
  160. );
  161. }
  162. });
  163. // Handle typedoc config output.
  164. const tdOptionsPath = path.join(pkgPath, 'tdoptions.json');
  165. if (fs.existsSync(tdOptionsPath)) {
  166. const tdConfigData = utils.readJSONFile(tdOptionsPath);
  167. const pkgDirName = pkgPath.split('/').pop();
  168. tdConfigData['out'] = `../../docs/api/${pkgDirName}`;
  169. utils.writeJSONFile(tdOptionsPath, tdConfigData);
  170. }
  171. // Handle references.
  172. let references: { [key: string]: string } = Object.create(null);
  173. Object.keys(deps).forEach(name => {
  174. if (!(name in locals)) {
  175. return;
  176. }
  177. const target = locals[name];
  178. if (!fs.existsSync(path.join(target, 'tsconfig.json'))) {
  179. return;
  180. }
  181. let ref = path.relative(pkgPath, locals[name]);
  182. references[name] = ref.split(path.sep).join('/');
  183. });
  184. if (
  185. data.name.indexOf('example-') === -1 &&
  186. Object.keys(references).length > 0
  187. ) {
  188. const tsConfigPath = path.join(pkgPath, 'tsconfig.json');
  189. const tsConfigData = utils.readJSONFile(tsConfigPath);
  190. tsConfigData.references = [];
  191. Object.keys(references).forEach(name => {
  192. tsConfigData.references.push({ path: references[name] });
  193. });
  194. utils.writeJSONFile(tsConfigPath, tsConfigData);
  195. }
  196. // Get a list of all the published files.
  197. // This will not catch .js or .d.ts files if they have not been built,
  198. // but we primarily use this to check for files that are published as-is,
  199. // like styles, assets, and schemas.
  200. const published = new Set<string>(
  201. data.files
  202. ? data.files.reduce((acc: string[], curr: string) => {
  203. return acc.concat(glob.sync(path.join(pkgPath, curr)));
  204. }, [])
  205. : []
  206. );
  207. // Ensure that the `schema` directories match what is in the `package.json`
  208. const schemaDir = data.jupyterlab && data.jupyterlab.schemaDir;
  209. const schemas = glob.sync(
  210. path.join(pkgPath, schemaDir || 'schema', '*.json')
  211. );
  212. if (schemaDir && !schemas.length) {
  213. messages.push(`No schemas found in ${path.join(pkgPath, schemaDir)}.`);
  214. } else if (!schemaDir && schemas.length) {
  215. messages.push(`Schemas found, but no schema indicated in ${pkgPath}`);
  216. }
  217. for (let schema of schemas) {
  218. if (!published.has(schema)) {
  219. messages.push(`Schema ${schema} not published in ${pkgPath}`);
  220. }
  221. }
  222. // Ensure that the `style` directories match what is in the `package.json`
  223. const styles = glob.sync(path.join(pkgPath, 'style', '**/*.*'));
  224. for (let style of styles) {
  225. if (!published.has(style)) {
  226. messages.push(`Style file ${style} not published in ${pkgPath}`);
  227. }
  228. }
  229. // If we have styles, ensure that 'style' field is declared
  230. if (styles.length > 0) {
  231. if (data.style === undefined) {
  232. data.style = 'style/index.css';
  233. }
  234. }
  235. // Ensure that sideEffects are declared, and that any styles are covered
  236. if (styles.length > 0) {
  237. if (data.sideEffects === undefined) {
  238. messages.push(
  239. `Side effects not declared in ${pkgPath}, and styles are present.`
  240. );
  241. } else if (data.sideEffects === false) {
  242. messages.push(`Style files not included in sideEffects in ${pkgPath}`);
  243. }
  244. }
  245. // Ensure dependencies and dev dependencies.
  246. data.dependencies = deps;
  247. data.devDependencies = devDeps;
  248. if (Object.keys(data.dependencies).length === 0) {
  249. delete data.dependencies;
  250. }
  251. if (Object.keys(data.devDependencies).length === 0) {
  252. delete data.devDependencies;
  253. }
  254. // Make sure there are no gitHead keys, which are only temporary keys used
  255. // when a package is actually being published.
  256. delete data.gitHead;
  257. // Ensure there is a minimal prepublishOnly script
  258. if (!data.private && !data.scripts.prepublishOnly) {
  259. messages.push(`prepublishOnly script missing in ${pkgPath}`);
  260. data.scripts.prepublishOnly = 'npm run build';
  261. }
  262. if (utils.writePackageData(path.join(pkgPath, 'package.json'), data)) {
  263. messages.push('Updated package.json');
  264. }
  265. return messages;
  266. }
  267. /**
  268. * An extra ensure function just for the @jupyterlab/ui-components package.
  269. * Ensures that the icon svg import statements are synced with the contents
  270. * of ui-components/style/icons.
  271. *
  272. * @param pkgPath - The path to the @jupyterlab/ui-components package.
  273. *
  274. * @returns A list of changes that were made to ensure the package.
  275. */
  276. export async function ensureUiComponents(pkgPath: string): Promise<string[]> {
  277. let messages: string[] = [];
  278. const tab = ' ';
  279. const iconSrcDir = path.join(pkgPath, 'src/icon');
  280. const svgs = glob.sync(path.join(pkgPath, 'style/icons', '**/*.svg'));
  281. // build the per-icon import code
  282. let iconImportStatements: string[] = [];
  283. let iconModelDeclarations: string[] = [];
  284. svgs.forEach(svg => {
  285. const name = utils.stem(svg);
  286. const nameCamel = utils.camelCase(name) + 'Svg';
  287. iconImportStatements.push(
  288. `import ${nameCamel} from '${path.relative(iconSrcDir, svg)}';`
  289. );
  290. iconModelDeclarations.push(
  291. tab + tab + `{ name: '${name}', svg: ${nameCamel} }`
  292. );
  293. });
  294. // generate the actual iconImports file
  295. let iconImports = generatedHeader('ensureUiComponents') + '\n\n';
  296. iconImports += "import { Icon } from './interfaces';\n\n";
  297. iconImports += '// icon svg import statements\n';
  298. iconImports += iconImportStatements.join('\n') + '\n\n';
  299. iconImports += '// defaultIcons definition\n';
  300. iconImports += 'export namespace IconImports {\n';
  301. iconImports +=
  302. tab + 'export const defaultIcons: ReadonlyArray<Icon.IModel> = [\n';
  303. iconImports += iconModelDeclarations.join(',\n') + '\n';
  304. iconImports += tab + '];\n';
  305. iconImports += '}\n';
  306. // write the iconImports file
  307. const iconImportsPath = path.join(iconSrcDir, 'iconImports.ts');
  308. const prev = fs.readFileSync(iconImportsPath, { encoding: 'utf8' });
  309. if (prev !== iconImports) {
  310. messages.push(`Updated ./${iconImportsPath}`);
  311. fs.writeFileSync(iconImportsPath, iconImports);
  312. }
  313. return messages;
  314. }
  315. /**
  316. * The options used to ensure a package.
  317. */
  318. export interface IEnsurePackageOptions {
  319. /**
  320. * The path to the package.
  321. */
  322. pkgPath: string;
  323. /**
  324. * The package data.
  325. */
  326. data: any;
  327. /**
  328. * The cache of dependency versions by package.
  329. */
  330. depCache?: { [key: string]: string };
  331. /**
  332. * A list of dependencies that can be unused.
  333. */
  334. unused?: string[];
  335. /**
  336. * A list of dependencies that can be missing.
  337. */
  338. missing?: string[];
  339. /**
  340. * A map of local package names and their relative path.
  341. */
  342. locals?: { [key: string]: string };
  343. /**
  344. * Whether to enforce that dependencies get used. Default is true.
  345. */
  346. noUnused?: boolean;
  347. /**
  348. * The css import list for the package.
  349. */
  350. cssImports?: string[];
  351. /**
  352. * Packages which are allowed to have multiple versions pulled in
  353. */
  354. differentVersions?: string[];
  355. }
  356. /**
  357. * Extract the module imports from a TypeScript source file.
  358. *
  359. * @param sourceFile - The path to the source file.
  360. *
  361. * @returns An array of package names.
  362. */
  363. function getImports(sourceFile: ts.SourceFile): string[] {
  364. let imports: string[] = [];
  365. handleNode(sourceFile);
  366. function handleNode(node: any): void {
  367. switch (node.kind) {
  368. case ts.SyntaxKind.ImportDeclaration:
  369. imports.push(node.moduleSpecifier.text);
  370. break;
  371. case ts.SyntaxKind.ImportEqualsDeclaration:
  372. imports.push(node.moduleReference.expression.text);
  373. break;
  374. default:
  375. // no-op
  376. }
  377. ts.forEachChild(node, handleNode);
  378. }
  379. return imports;
  380. }