#!/usr/bin/env node /** * scripts/vendor.mjs —— 把 lit 系依赖 vendor 到 lib/vendor/(方案 B:本地兜底)。 * * 为什么 vendor:Repose 依赖仓(/npm/importmap/lit-v3.json)在 repose.fmode.cn / * kangshifu.fmode.cn / app.fmode.cn 三个域名实测都是 404,见 README「依赖走哪条路」。 * * 做的事: * 1. npm pack 每个包(不 npm install,不留 node_modules) * 2. 解到 lib/vendor//v/,去掉 .ts / .js.map / src / development / node 等非运行时目录 * 3. 校验每个包的 package.json main 指向的入口文件真实存在 * * 可重复执行:先删后写,结果由 VENDOR 表唯一决定。 * 用法:node scripts/vendor.mjs [--offline ] */ import { execFileSync } from 'node:child_process'; import { mkdtempSync, rmSync, mkdirSync, readFileSync, existsSync, cpSync, readdirSync, statSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; export const VENDOR = [ { name: 'lit', version: '3.3.3' }, { name: 'lit-html', version: '3.3.3' }, { name: 'lit-element', version: '4.2.2' }, { name: '@lit/reactive-element', version: '2.1.2' }, { name: '@lit/context', version: '1.1.6' }, { name: '@lit-labs/signals', version: '0.3.0' }, { name: 'signal-polyfill', version: '0.2.2' }, ]; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); const OUT = join(ROOT, 'lib/vendor'); /** 运行时用不到的目录/文件:源码、类型、调试版、Node 专用实现 */ const DROP = [ /\/development\//, /\/node\//, /\/src\//, /\/decorators\//, /\/test\//, /\.ts$/, /\.d\.ts$/, /\.js\.map$/, /\.tsbuildinfo$/, /\/cjs\//, ]; const offlineIdx = process.argv.indexOf('--offline'); const offlineDir = offlineIdx > -1 ? process.argv[offlineIdx + 1] : null; function tgzName(name, version) { return `${name.replace('/', '-').replace('@', '')}-${version}.tgz`; } function findTgz(name, version, dir) { const direct = join(dir, `${name.replace(/^@/, '').replace('/', '-')}-${version}.tgz`); if (existsSync(direct)) return direct; const fallback = join(dir, tgzName(name, version)); if (existsSync(fallback)) return fallback; return null; } const work = mkdtempSync(join(tmpdir(), 'kf-vendor-')); let failed = 0; rmSync(OUT, { recursive: true, force: true }); mkdirSync(OUT, { recursive: true }); for (const pkg of VENDOR) { const id = `${pkg.name}@${pkg.version}`; let tgz = offlineDir ? findTgz(pkg.name, pkg.version, offlineDir) : null; if (!tgz) { // 不带路径输出,避免把文件落在 cwd const fetched = execFileSync('npm', ['pack', id, '--silent', '--pack-destination', work], { encoding: 'utf8' }).trim().split('\n').pop(); tgz = join(work, fetched); } const stage = join(work, pkg.name.replace(/[/@]/g, '_') + '-' + pkg.version); mkdirSync(stage, { recursive: true }); execFileSync('tar', ['xzf', tgz, '-C', stage]); const dest = join(OUT, pkg.name, `v${pkg.version}`); mkdirSync(dest, { recursive: true }); cpSync(join(stage, 'package'), dest, { recursive: true, filter: (src) => { if (!src.startsWith(stage)) return true; const rel = '/' + src.slice(join(stage, 'package').length).replace(/^[/\\]/, '').replace(/\\/g, '/'); return !DROP.some((re) => re.test(rel + '/')); }, }); const pj = JSON.parse(readFileSync(join(dest, 'package.json'), 'utf8')); if (pj.version !== pkg.version) { console.error(`✗ ${id}: 包内版本是 ${pj.version}`); failed++; continue; } // 入口文件必须存在(importmap 指向的就是它) const entries = new Set(); const ex = pj.exports?.['.']; if (typeof ex === 'string') entries.add(ex); else if (ex?.import?.default) entries.add(ex.import.default); else if (ex?.default) entries.add(ex.default); if (pj.main) entries.add(pj.main); const missing = [...entries].filter((e) => !existsSync(join(dest, e))); if (missing.length) { console.error(`✗ ${id}: 入口不存在 ${missing.join(', ')}`); failed++; continue; } const size = du(dest); console.log(`✓ ${id} → lib/vendor/${pkg.name}/v${pkg.version}/ (${entries.values().next().value}, ${size})`); } rmSync(work, { recursive: true, force: true }); console.log(failed ? `\nVENDOR FAILED(${failed})` : '\nVENDOR OK'); process.exit(failed ? 1 : 0); function du(dir) { let n = 0; for (const e of readdirSync(dir)) { const p = join(dir, e); const s = statSync(p); n += s.isDirectory() ? du(p) : s.size; } return `${Math.round(n / 1024) || "<1"} KB`; }