vendor.mjs 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. #!/usr/bin/env node
  2. /**
  3. * scripts/vendor.mjs —— 把 lit 系依赖 vendor 到 lib/vendor/(方案 B:本地兜底)。
  4. *
  5. * 为什么 vendor:Repose 依赖仓(/npm/importmap/lit-v3.json)在 repose.fmode.cn /
  6. * kangshifu.fmode.cn / app.fmode.cn 三个域名实测都是 404,见 README「依赖走哪条路」。
  7. *
  8. * 做的事:
  9. * 1. npm pack 每个包(不 npm install,不留 node_modules)
  10. * 2. 解到 lib/vendor/<name>/v<version>/,去掉 .ts / .js.map / src / development / node 等非运行时目录
  11. * 3. 校验每个包的 package.json main 指向的入口文件真实存在
  12. *
  13. * 可重复执行:先删后写,结果由 VENDOR 表唯一决定。
  14. * 用法:node scripts/vendor.mjs [--offline <tgz目录>]
  15. */
  16. import { execFileSync } from 'node:child_process';
  17. import { mkdtempSync, rmSync, mkdirSync, readFileSync, existsSync, cpSync, readdirSync, statSync } from 'node:fs';
  18. import { tmpdir } from 'node:os';
  19. import { join, dirname } from 'node:path';
  20. import { fileURLToPath } from 'node:url';
  21. export const VENDOR = [
  22. { name: 'lit', version: '3.3.3' },
  23. { name: 'lit-html', version: '3.3.3' },
  24. { name: 'lit-element', version: '4.2.2' },
  25. { name: '@lit/reactive-element', version: '2.1.2' },
  26. { name: '@lit/context', version: '1.1.6' },
  27. { name: '@lit-labs/signals', version: '0.3.0' },
  28. { name: 'signal-polyfill', version: '0.2.2' },
  29. ];
  30. const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
  31. const OUT = join(ROOT, 'lib/vendor');
  32. /** 运行时用不到的目录/文件:源码、类型、调试版、Node 专用实现 */
  33. const DROP = [
  34. /\/development\//, /\/node\//, /\/src\//, /\/decorators\//, /\/test\//,
  35. /\.ts$/, /\.d\.ts$/, /\.js\.map$/, /\.tsbuildinfo$/, /\/cjs\//,
  36. ];
  37. const offlineIdx = process.argv.indexOf('--offline');
  38. const offlineDir = offlineIdx > -1 ? process.argv[offlineIdx + 1] : null;
  39. function tgzName(name, version) {
  40. return `${name.replace('/', '-').replace('@', '')}-${version}.tgz`;
  41. }
  42. function findTgz(name, version, dir) {
  43. const direct = join(dir, `${name.replace(/^@/, '').replace('/', '-')}-${version}.tgz`);
  44. if (existsSync(direct)) return direct;
  45. const fallback = join(dir, tgzName(name, version));
  46. if (existsSync(fallback)) return fallback;
  47. return null;
  48. }
  49. const work = mkdtempSync(join(tmpdir(), 'kf-vendor-'));
  50. let failed = 0;
  51. rmSync(OUT, { recursive: true, force: true });
  52. mkdirSync(OUT, { recursive: true });
  53. for (const pkg of VENDOR) {
  54. const id = `${pkg.name}@${pkg.version}`;
  55. let tgz = offlineDir ? findTgz(pkg.name, pkg.version, offlineDir) : null;
  56. if (!tgz) {
  57. // 不带路径输出,避免把文件落在 cwd
  58. const fetched = execFileSync('npm', ['pack', id, '--silent', '--pack-destination', work], { encoding: 'utf8' }).trim().split('\n').pop();
  59. tgz = join(work, fetched);
  60. }
  61. const stage = join(work, pkg.name.replace(/[/@]/g, '_') + '-' + pkg.version);
  62. mkdirSync(stage, { recursive: true });
  63. execFileSync('tar', ['xzf', tgz, '-C', stage]);
  64. const dest = join(OUT, pkg.name, `v${pkg.version}`);
  65. mkdirSync(dest, { recursive: true });
  66. cpSync(join(stage, 'package'), dest, {
  67. recursive: true,
  68. filter: (src) => {
  69. if (!src.startsWith(stage)) return true;
  70. const rel = '/' + src.slice(join(stage, 'package').length).replace(/^[/\\]/, '').replace(/\\/g, '/');
  71. return !DROP.some((re) => re.test(rel + '/'));
  72. },
  73. });
  74. const pj = JSON.parse(readFileSync(join(dest, 'package.json'), 'utf8'));
  75. if (pj.version !== pkg.version) {
  76. console.error(`✗ ${id}: 包内版本是 ${pj.version}`);
  77. failed++;
  78. continue;
  79. }
  80. // 入口文件必须存在(importmap 指向的就是它)
  81. const entries = new Set();
  82. const ex = pj.exports?.['.'];
  83. if (typeof ex === 'string') entries.add(ex);
  84. else if (ex?.import?.default) entries.add(ex.import.default);
  85. else if (ex?.default) entries.add(ex.default);
  86. if (pj.main) entries.add(pj.main);
  87. const missing = [...entries].filter((e) => !existsSync(join(dest, e)));
  88. if (missing.length) {
  89. console.error(`✗ ${id}: 入口不存在 ${missing.join(', ')}`);
  90. failed++;
  91. continue;
  92. }
  93. const size = du(dest);
  94. console.log(`✓ ${id} → lib/vendor/${pkg.name}/v${pkg.version}/ (${entries.values().next().value}, ${size})`);
  95. }
  96. rmSync(work, { recursive: true, force: true });
  97. console.log(failed ? `\nVENDOR FAILED(${failed})` : '\nVENDOR OK');
  98. process.exit(failed ? 1 : 0);
  99. function du(dir) {
  100. let n = 0;
  101. for (const e of readdirSync(dir)) {
  102. const p = join(dir, e);
  103. const s = statSync(p);
  104. n += s.isDirectory() ? du(p) : s.size;
  105. }
  106. return `${Math.round(n / 1024) || "<1"} KB`;
  107. }