create-file.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. // @ts-check
  2. import { readFileByLine } from './fetch-text-by-line';
  3. import { surgeDomainsetToClashDomainset, surgeRulesetToClashClassicalTextRuleset } from './clash';
  4. import picocolors from 'picocolors';
  5. import type { Span } from '../trace';
  6. import path from 'path';
  7. export async function compareAndWriteFile(span: Span, linesA: string[], filePath: string) {
  8. let isEqual = true;
  9. const file = Bun.file(filePath);
  10. const linesALen = linesA.length;
  11. if (!(await file.exists())) {
  12. console.log(`${filePath} does not exists, writing...`);
  13. isEqual = false;
  14. } else if (linesALen === 0) {
  15. console.log(`Nothing to write to ${filePath}...`);
  16. isEqual = false;
  17. } else {
  18. isEqual = await span.traceChildAsync(`comparing ${filePath}`, async () => {
  19. let index = 0;
  20. for await (const lineB of readFileByLine(file)) {
  21. const lineA = linesA[index];
  22. index++;
  23. if (lineA == null) {
  24. // The file becomes smaller
  25. return false;
  26. }
  27. if (lineA[0] === '#' && lineB[0] === '#') {
  28. continue;
  29. }
  30. if (
  31. lineA[0] === '/'
  32. && lineA[1] === '/'
  33. && lineB[0] === '/'
  34. && lineB[1] === '/'
  35. && lineA[3] === '#'
  36. && lineB[3] === '#'
  37. ) {
  38. continue;
  39. }
  40. if (lineA !== lineB) {
  41. return false;
  42. }
  43. }
  44. if (index !== linesALen) {
  45. // The file becomes larger
  46. return false;
  47. }
  48. return true;
  49. });
  50. }
  51. if (isEqual) {
  52. console.log(picocolors.dim(`same content, bail out writing: ${filePath}`));
  53. return;
  54. }
  55. await span.traceChildAsync(`writing ${filePath}`, async () => {
  56. if (linesALen < 10000) {
  57. return Bun.write(file, `${linesA.join('\n')}\n`);
  58. }
  59. const writer = file.writer();
  60. for (let i = 0; i < linesALen; i++) {
  61. writer.write(linesA[i]);
  62. writer.write('\n');
  63. }
  64. return writer.end();
  65. });
  66. }
  67. export const withBannerArray = (title: string, description: string[] | readonly string[], date: Date, content: string[]) => {
  68. return [
  69. '#########################################',
  70. `# ${title}`,
  71. `# Last Updated: ${date.toISOString()}`,
  72. `# Size: ${content.length}`,
  73. ...description.map(line => (line ? `# ${line}` : '#')),
  74. '#########################################',
  75. ...content,
  76. '################## EOF ##################'
  77. ];
  78. };
  79. export const createRuleset = (
  80. parentSpan: Span,
  81. title: string, description: string[] | readonly string[], date: Date, content: string[],
  82. type: 'ruleset' | 'domainset', surgePath: string, clashPath: string
  83. ) => parentSpan.traceChild(`create ruleset: ${path.basename(surgePath, path.extname(surgePath))}`).traceAsyncFn((childSpan) => {
  84. const surgeContent = withBannerArray(title, description, date, content);
  85. const clashContent = childSpan.traceChildSync('convert incoming ruleset to clash', () => {
  86. let _clashContent;
  87. switch (type) {
  88. case 'domainset':
  89. _clashContent = surgeDomainsetToClashDomainset(content);
  90. break;
  91. case 'ruleset':
  92. _clashContent = surgeRulesetToClashClassicalTextRuleset(content);
  93. break;
  94. default:
  95. throw new TypeError(`Unknown type: ${type as any}`);
  96. }
  97. return withBannerArray(title, description, date, _clashContent);
  98. });
  99. return Promise.all([
  100. compareAndWriteFile(childSpan, surgeContent, surgePath),
  101. compareAndWriteFile(childSpan, clashContent, clashPath)
  102. ]);
  103. });