build-common.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. // @ts-check
  2. import * as path from 'path';
  3. import { PathScurry } from 'path-scurry';
  4. import { readFileByLine } from './lib/fetch-text-by-line';
  5. import { processLine } from './lib/process-line';
  6. import { createRuleset } from './lib/create-file';
  7. import { domainDeduper } from './lib/domain-deduper';
  8. import type { Span } from './trace';
  9. import { task } from './trace';
  10. import { SHARED_DESCRIPTION } from './lib/constants';
  11. import picocolors from 'picocolors';
  12. const MAGIC_COMMAND_SKIP = '# $ custom_build_script';
  13. const MAGIC_COMMAND_TITLE = '# $ meta_title ';
  14. const MAGIC_COMMAND_DESCRIPTION = '# $ meta_description ';
  15. const sourceDir = path.resolve(import.meta.dir, '../Source');
  16. const outputSurgeDir = path.resolve(import.meta.dir, '../List');
  17. const outputClashDir = path.resolve(import.meta.dir, '../Clash');
  18. export const buildCommon = task(import.meta.path, async (span) => {
  19. const promises: Array<Promise<unknown>> = [];
  20. const pw = new PathScurry(sourceDir);
  21. for await (const entry of pw) {
  22. if (!entry.isFile()) {
  23. continue;
  24. }
  25. const extname = path.extname(entry.name);
  26. if (extname === '.js' || extname === '.ts') {
  27. continue;
  28. }
  29. const relativePath = entry.relative();
  30. if (relativePath.startsWith('domainset/')) {
  31. promises.push(transformDomainset(span, entry.fullpath(), relativePath));
  32. continue;
  33. }
  34. if (
  35. relativePath.startsWith('ip/')
  36. || relativePath.startsWith('non_ip/')
  37. ) {
  38. promises.push(transformRuleset(span, entry.fullpath(), relativePath));
  39. continue;
  40. }
  41. console.error(picocolors.red(`[build-comman] Unknown file: ${relativePath}`));
  42. }
  43. return Promise.all(promises);
  44. });
  45. if (import.meta.main) {
  46. buildCommon();
  47. }
  48. const processFile = (span: Span, sourcePath: string) => {
  49. // console.log('Processing', sourcePath);
  50. return span.traceChildAsync(`process file: ${sourcePath}`, async () => {
  51. const lines: string[] = [];
  52. let title = '';
  53. const descriptions: string[] = [];
  54. try {
  55. for await (const line of readFileByLine(sourcePath)) {
  56. if (line.startsWith(MAGIC_COMMAND_SKIP)) {
  57. return null;
  58. }
  59. if (line.startsWith(MAGIC_COMMAND_TITLE)) {
  60. title = line.slice(MAGIC_COMMAND_TITLE.length).trim();
  61. continue;
  62. }
  63. if (line.startsWith(MAGIC_COMMAND_DESCRIPTION)) {
  64. descriptions.push(line.slice(MAGIC_COMMAND_DESCRIPTION.length).trim());
  65. continue;
  66. }
  67. const l = processLine(line);
  68. if (l) {
  69. lines.push(l);
  70. }
  71. }
  72. } catch (e) {
  73. console.error('Error processing', sourcePath);
  74. console.trace(e);
  75. }
  76. return [title, descriptions, lines] as const;
  77. });
  78. };
  79. function transformDomainset(parentSpan: Span, sourcePath: string, relativePath: string) {
  80. return parentSpan
  81. .traceChildAsync(
  82. `transform domainset: ${path.basename(sourcePath, path.extname(sourcePath))}`,
  83. async (span) => {
  84. const res = await processFile(span, sourcePath);
  85. if (!res) return;
  86. const [title, descriptions, lines] = res;
  87. const deduped = domainDeduper(lines);
  88. const description = [
  89. ...SHARED_DESCRIPTION,
  90. ...(
  91. descriptions.length
  92. ? ['', ...descriptions]
  93. : []
  94. )
  95. ];
  96. return createRuleset(
  97. span,
  98. title,
  99. description,
  100. new Date(),
  101. deduped,
  102. 'domainset',
  103. path.resolve(outputSurgeDir, relativePath),
  104. path.resolve(outputClashDir, `${relativePath.slice(0, -path.extname(relativePath).length)}.txt`)
  105. );
  106. }
  107. );
  108. }
  109. /**
  110. * Output Surge RULE-SET and Clash classical text format
  111. */
  112. async function transformRuleset(parentSpan: Span, sourcePath: string, relativePath: string) {
  113. return parentSpan
  114. .traceChild(`transform ruleset: ${path.basename(sourcePath, path.extname(sourcePath))}`)
  115. .traceAsyncFn(async (span) => {
  116. const res = await processFile(span, sourcePath);
  117. if (!res) return null;
  118. const [title, descriptions, lines] = res;
  119. const description = [
  120. ...SHARED_DESCRIPTION,
  121. ...(
  122. descriptions.length
  123. ? ['', ...descriptions]
  124. : []
  125. )
  126. ];
  127. return createRuleset(
  128. span,
  129. title,
  130. description,
  131. new Date(),
  132. lines,
  133. 'ruleset',
  134. path.resolve(outputSurgeDir, relativePath),
  135. path.resolve(outputClashDir, `${relativePath.slice(0, -path.extname(relativePath).length)}.txt`)
  136. );
  137. });
  138. }