import { arrayEach } from 'jsc-utils/array'; import { objectAssign } from 'jsc-utils/object'; import { isArray, isBoolean, isObject, isString } from 'jsc-utils/type'; import http from 'http'; import path from 'path'; import fse from 'fs-extra'; import { gte, lt } from 'semver'; import { isFile } from './file-system'; import { nsDir } from './ns'; import path2 from './path2'; type RegistryType = 'npm' | 'taobao' | 'isyscore' | string; const registryStore = { npm: 'http://registry.npmjs.com', taobao: 'http://registry.npm.taobao.org', isyscore: 'http://npm.isyscore.net' }; export const getPkgUrl = (name: string, registry: RegistryType = 'taobao') => { const safeName = encodeURIComponent(name); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/ban-ts-comment // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const url = registryStore[registry] || registry; // eslint-disable-next-line @typescript-eslint/restrict-template-expressions return `${url}/${safeName}`; }; export interface PkgInfo { name: string; distTags: { [prop: string]: string; }; } export interface PkgInfoOptions { registry: RegistryType; } interface PkgJson { error: ''; 'dist-tags': { [prop: string]: string; }; } export const pkgInfo = async (name: string, options?: Partial): Promise => { const defaults: PkgInfoOptions = { registry: 'taobao' }; const { registry } = objectAssign(defaults, options); const pkgUrl = getPkgUrl(name, registry); return new Promise((resolve, reject) => { const wrapError = new Error(`${name}(${pkgUrl})包不存在`); const req = http.request(pkgUrl, (res) => { const bufferList: Array = []; res // 折行 .on('data', (data) => { bufferList.push(data); }) .on('end', () => { const buffer = Buffer.concat(bufferList); try { const json = JSON.parse(buffer.toString()) as PkgJson; const { error } = json; if (typeof error === 'string') { return reject(wrapError); } // console.log(pkgUrl); // console.log(json); const { ['dist-tags']: distTags } = json; resolve({ name, distTags }); } catch (err) { return reject(wrapError); } }) .on('error', (err) => reject(wrapError)); }); req.on('error', (err) => reject(wrapError)); req.end(); }); }; export interface PkgChecker { name: string; lastCheckTime: string; checkTimes: number; } export interface PkgCanCheckOptions { // 间隔天数 days: number; // 间隔次数 times: number; } export const pkgCanCheck = (name: string, options?: Partial): boolean => { const defaults: PkgCanCheckOptions = { days: 7, times: 10 }; const safeName = encodeURIComponent(name); const options2 = objectAssign(defaults, options); const { days, times } = options2; const cacheFile = path.join(nsDir('utils'), 'pkg-checker', `${safeName}.json`); let checker: PkgChecker; let firstCheck = false; const updateCache = () => { try { fse.outputJsonSync(cacheFile, checker); } catch (err) { // ignore } }; try { checker = fse.readJsonSync(cacheFile) as PkgChecker; } catch (err) { firstCheck = true; checker = { name, lastCheckTime: new Date().toISOString(), checkTimes: 1 }; } const overDays = Date.now() - new Date(checker.lastCheckTime).getTime() >= days * 24 * 60 * 60 * 1000; const overTimes = checker.checkTimes > times; const canCheck = overDays || overTimes; checker.lastCheckTime = new Date().toISOString(); if (!firstCheck) checker.checkTimes++; if (canCheck) checker.checkTimes = 1; updateCache(); return canCheck; }; export interface PkgTestUpdateOptions { distTag: 'latest' | 'alpha' | 'beta' | 'rc' | 'next' | string; registry: RegistryType; throwError: boolean; test: (localVersion: string, onlineVersion: string) => boolean; } export const pkgTestUpdate = async ( name: string, localVersion: string, options?: Partial ): Promise => { const defaults: PkgTestUpdateOptions = { distTag: 'latest', registry: 'taobao', throwError: false, test: (localVersion, onlineVersion) => localVersion !== onlineVersion }; const options2 = objectAssign(defaults, options); const { distTag, registry, throwError } = options2; try { const { distTags: { [distTag]: onlineVersion } } = await pkgInfo(name, { registry }); if (onlineVersion) { if (options2.test(localVersion, onlineVersion)) return onlineVersion; else return null; } else { if (throwError) throw new Error(`${name} 的 ${distTag} 标签版本不存在`); else return null; } } catch (err) { if (throwError) throw err; else return null; } }; interface ModuleMeta { isPkg: boolean; scope: string; name: string; pkgName: string; entry: string; } /** * 解析模块信息 * - ./abc => {isPkg: false, scope: "", name: "", pkgName: "", entry: "./abc"} * - abc => {isPkg: true, scope: "", name: "abc", pkgName: "abc", entry: "."} * - abc/def => {isPkg: true, scope: "", name: "abc", pkgName: "abc", entry: "def"} * - @abc/def => {isPkg: true, scope: "@abc", name: "def", pkgName: "@abc/def", entry: "."} * - @abc/def/ghi => {isPkg: true, scope: "@abc", name: "def", pkgName: "@abc/def", entry: "ghi"} * @param {string} module * @returns {ModuleMeta} */ export function parseModule(module: string): ModuleMeta { // ./path/to/file // ../path/to/file // /path/to/file // d:/path/to/file const isFile = /^(.+:)?\.{0,2}\//.test(path2.standardize(module)); if (isFile) { return { isPkg: false, scope: '', name: module, pkgName: '', entry: module }; } const matches = /^(?:(@[^/]+)\/)?([^/]+)/.exec(module); if (!matches) { return { isPkg: false, scope: '', name: module, pkgName: '', entry: module }; } const [, scope = '', name] = matches; const pkgName = scope ? `${scope}/${name}` : name; return { isPkg: true, scope, name, pkgName, entry: path2.relative(pkgName, module) }; } interface EsModule { __esModule: boolean; default: T; } interface RequireOptions { dependency: string; dev: boolean; minVersion: string; maxVersion: string; } /** * 导入运行时模块 * @param {string} module * @param {Partial} options * @returns {R} */ export function requireModule(module: string, options?: Partial): R { // eslint-disable-next-line prefer-rest-params const { isPkg, pkgName } = parseModule(module); const { // ... dependency = pkgName, dev = false, minVersion = '', maxVersion = '' } = options || {}; let mod; const versionRange = []; if (minVersion) versionRange.push(`>=${minVersion}`); if (maxVersion) versionRange.push(`<${maxVersion}`); const version = versionRange.length > 0 ? `@${JSON.stringify(versionRange.join(' '))}` : ''; try { // eslint-disable-next-line @typescript-eslint/no-var-requires mod = require(module) as R & EsModule; } catch (err) { if (isPkg) { const message = `依赖 ${pkgName} 模块,需要安装 ${dependency}${version} ${dev ? '开发' : '生产'}依赖`; throw new Error(message); } else { throw err; } } const testVersion = isPkg && Boolean(minVersion || maxVersion); if (testVersion) { const { version } = requirePackageJson(module); if (minVersion && !gte(version, minVersion)) { throw new Error(`需要安装 ${dependency} 在 ${minVersion} 及以上`); } if (maxVersion && !lt(version, maxVersion)) { throw new Error(`安装的 ${dependency} 依赖不能超过 ${maxVersion} 版本`); } } if (typeof mod === 'object' && mod.__esModule && typeof mod.default !== 'undefined') return mod.default; else return mod as R; } /** * 使用 requireModule 代替 * @param {string} module * @returns {T} */ export function importDefault(module: string): T { return requireModule(module); } export interface NpmPackageJson { name: string; version: string; } export function requirePackageJson(module: string, context?: string | string[]): T { context = context || path.resolve('node_modules'); const contexts = isArray(context) ? context : [context]; const find = (contexts: string[]): string | null => { let found: string | null = null; arrayEach(contexts, (context) => { const file = path.join(context, module, 'package.json'); if (isFile(file)) { found = file; return false; } }); return found; }; const found = find(contexts); if (found === null) { throw new Error(`未发现 ${module} 所在 npm 模块的描述文件(package.json)`); } // eslint-disable-next-line @typescript-eslint/no-var-requires const pkgJson = require(found) as T; delete require.cache[found]; return pkgJson; }