index.ts 4.2 KB

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