index.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. import type { CAC } from 'cac';
  2. import type { Result } from 'publint';
  3. import { basename, dirname, join } from 'node:path';
  4. import {
  5. colors,
  6. consola,
  7. ensureFile,
  8. findMonorepoRoot,
  9. generatorContentHash,
  10. getPackages,
  11. outputJSON,
  12. readJSON,
  13. UNICODE,
  14. } from '@vben/node-utils';
  15. import { publint } from 'publint';
  16. import { formatMessage } from 'publint/utils';
  17. const CACHE_FILE = join(
  18. 'node_modules',
  19. '.cache',
  20. 'publint',
  21. '.pkglintcache.json',
  22. );
  23. interface PubLintCommandOptions {
  24. /**
  25. * Only errors are checked, no program exit is performed
  26. */
  27. check?: boolean;
  28. }
  29. /**
  30. * Get files that require lint
  31. * @param files
  32. */
  33. async function getLintFiles(files: string[] = []) {
  34. const lintFiles: string[] = [];
  35. if (files?.length > 0) {
  36. return files.filter((file) => basename(file) === 'package.json');
  37. }
  38. const { packages } = await getPackages();
  39. for (const { dir } of packages) {
  40. lintFiles.push(join(dir, 'package.json'));
  41. }
  42. return lintFiles;
  43. }
  44. function getCacheFile() {
  45. const root = findMonorepoRoot();
  46. return join(root, CACHE_FILE);
  47. }
  48. async function readCache(cacheFile: string) {
  49. try {
  50. await ensureFile(cacheFile);
  51. return await readJSON(cacheFile);
  52. } catch {
  53. return {};
  54. }
  55. }
  56. async function runPublint(files: string[], { check }: PubLintCommandOptions) {
  57. const lintFiles = await getLintFiles(files);
  58. const cacheFile = getCacheFile();
  59. const cacheData = await readCache(cacheFile);
  60. const cache: Record<string, { hash: string; result: Result }> = cacheData;
  61. const results = await Promise.all(
  62. lintFiles.map(async (file) => {
  63. try {
  64. const pkgJson = await readJSON(file);
  65. if (pkgJson.private) {
  66. return null;
  67. }
  68. Reflect.deleteProperty(pkgJson, 'dependencies');
  69. Reflect.deleteProperty(pkgJson, 'devDependencies');
  70. Reflect.deleteProperty(pkgJson, 'peerDependencies');
  71. const content = JSON.stringify(pkgJson);
  72. const hash = generatorContentHash(content);
  73. const publintResult: Result =
  74. cache?.[file]?.hash === hash
  75. ? (cache?.[file]?.result ?? [])
  76. : await publint({
  77. level: 'suggestion',
  78. pkgDir: dirname(file),
  79. strict: true,
  80. });
  81. cache[file] = {
  82. hash,
  83. result: publintResult,
  84. };
  85. return { pkgJson, pkgPath: file, publintResult };
  86. } catch {
  87. return null;
  88. }
  89. }),
  90. );
  91. await outputJSON(cacheFile, cache);
  92. printResult(results, check);
  93. }
  94. function printResult(
  95. results: Array<null | {
  96. pkgJson: Record<string, number | string>;
  97. pkgPath: string;
  98. publintResult: Result;
  99. }>,
  100. check?: boolean,
  101. ) {
  102. let errorCount = 0;
  103. let warningCount = 0;
  104. let suggestionsCount = 0;
  105. for (const result of results) {
  106. if (!result) {
  107. continue;
  108. }
  109. const { pkgJson, pkgPath, publintResult } = result;
  110. const messages = publintResult?.messages ?? [];
  111. if (messages?.length < 1) {
  112. continue;
  113. }
  114. consola.log('');
  115. consola.log(pkgPath);
  116. for (const message of messages) {
  117. switch (message.type) {
  118. case 'error': {
  119. errorCount++;
  120. break;
  121. }
  122. case 'suggestion': {
  123. suggestionsCount++;
  124. break;
  125. }
  126. case 'warning': {
  127. warningCount++;
  128. break;
  129. }
  130. // No default
  131. }
  132. const ruleUrl = `https://publint.dev/rules#${message.code.toLocaleLowerCase()}`;
  133. consola.log(
  134. ` ${formatMessage(message, pkgJson)}${colors.dim(` ${ruleUrl}`)}`,
  135. );
  136. }
  137. }
  138. const totalCount = warningCount + errorCount + suggestionsCount;
  139. if (totalCount > 0) {
  140. consola.error(
  141. colors.red(
  142. `${UNICODE.FAILURE} ${totalCount} problem (${errorCount} errors, ${warningCount} warnings, ${suggestionsCount} suggestions)`,
  143. ),
  144. );
  145. !check && process.exit(1);
  146. } else {
  147. consola.log(colors.green(`${UNICODE.SUCCESS} No problem`));
  148. }
  149. }
  150. function definePubLintCommand(cac: CAC) {
  151. cac
  152. .command('publint [...files]')
  153. .usage('Check if the monorepo package conforms to the publint standard.')
  154. .option('--check', 'Only errors are checked, no program exit is performed.')
  155. .action(runPublint);
  156. }
  157. export { definePubLintCommand };