diff --git a/lib/services/install/npm-resolve.js b/lib/services/install/npm-resolve.js index 72aa3ee..7abdb37 100644 --- a/lib/services/install/npm-resolve.js +++ b/lib/services/install/npm-resolve.js @@ -29,11 +29,16 @@ const SEARCH_RESULT_LIMIT = 250; * 保证「配置了镜像」时反查和安装走同一个源(镜像节点同步完整时结果一致)。 * 慢网络下失败不阻塞安装。 */ -export function resolveNpmPackage(repo, registry = '') { +export async function resolveNpmPackage(repo, registry = '') { const key = repo.toLowerCase(); if (cache.has(key)) return Promise.resolve(cache.get(key) ?? null); - const name = searchRepo(repo, registry); + const name = searchRepo(repo, registry).then(async (found) => { + if (found !== null) + return found; + const canonical = await canonicalRepo(repo); + return canonical.toLowerCase() === key ? found : searchRepo(canonical, registry); + }); void name.then((found) => { if (found !== undefined) cache.set(key, found); @@ -65,7 +70,7 @@ export function isDshPackageMetadataForRepo(pkg, repo) { const manifestMarked = pkg.dsh !== null && typeof pkg.dsh === 'object'; return keywordMarked || manifestMarked; } -export function isDshNpmPackageForRepo(packageName, repo, registry = '') { +export async function isDshNpmPackageForRepo(packageName, repo, registry = '') { if (packageName === '' || repo === '') return Promise.resolve(false); const base = registry === '' ? 'https://registry.npmjs.org' : registry.replace(/\/+$/, ''); @@ -79,9 +84,11 @@ export function isDshNpmPackageForRepo(packageName, repo, registry = '') { } let body = ''; res.on('data', (chunk) => { body += chunk.toString(); }); - res.on('end', () => { + res.on('end', async () => { try { - resolve(isDshPackageMetadataForRepo(JSON.parse(body), repo)); + const pkg = JSON.parse(body); + resolve(isDshPackageMetadataForRepo(pkg, repo) + || isDshPackageMetadataForRepo(pkg, await canonicalRepo(repo))); } catch { resolve(false); @@ -92,6 +99,20 @@ export function isDshNpmPackageForRepo(packageName, repo, registry = '') { req.on('error', () => resolve(false)); }); } +async function canonicalRepo(repo) { + if (githubRepoOf(repo) === null) + return repo; + try { + const response = await fetch(`https://github.com/${repo}`, { + method: 'HEAD', + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + return response.ok ? githubRepoOf(response.url) ?? repo : repo; + } + catch { + return repo; + } +} function matchingPackagesForRepo(objects, repo) { const matches = []; for (const obj of objects) { diff --git a/lib/services/install/preflight.js b/lib/services/install/preflight.js index d04dfdd..9389b30 100644 --- a/lib/services/install/preflight.js +++ b/lib/services/install/preflight.js @@ -51,7 +51,9 @@ export async function preflightTarget(target) { return { ok: true, missing: null, name: null }; const entry = resolvePackageEntry(meta); const inTar = await hasTarEntry(tar, `${rootPrefix}${entry}`); - return { ok: inTar, missing: inTar ? null : entry, name: typeof meta.name === 'string' ? meta.name : null }; + const prepare = meta.scripts?.prepare; + const ok = inTar || (typeof prepare === 'string' && prepare.trim() !== ''); + return { ok, missing: ok ? null : entry, name: typeof meta.name === 'string' ? meta.name : null }; } catch { return { ok: true, missing: null, name: null }; diff --git a/src/server/services/install/npm-resolve.ts b/src/server/services/install/npm-resolve.ts index 8f79b79..25f612f 100644 --- a/src/server/services/install/npm-resolve.ts +++ b/src/server/services/install/npm-resolve.ts @@ -32,10 +32,14 @@ const SEARCH_RESULT_LIMIT = 250 * 保证「配置了镜像」时反查和安装走同一个源(镜像节点同步完整时结果一致)。 * 慢网络下失败不阻塞安装。 */ -export function resolveNpmPackage(repo: string, registry = ''): Promise { +export async function resolveNpmPackage(repo: string, registry = ''): Promise { const key = repo.toLowerCase() if (cache.has(key)) return Promise.resolve(cache.get(key) ?? null) - const name = searchRepo(repo, registry) + const name = searchRepo(repo, registry).then(async (found) => { + if (found !== null) return found + const canonical = await canonicalRepo(repo) + return canonical.toLowerCase() === key ? found : searchRepo(canonical, registry) + }) void name.then((found) => { if (found !== undefined) cache.set(key, found) }) @@ -77,7 +81,7 @@ export function isDshPackageMetadataForRepo(pkg: NpmSearchPackage, repo: string) return keywordMarked || manifestMarked } -export function isDshNpmPackageForRepo(packageName: string, repo: string, registry = ''): Promise { +export async function isDshNpmPackageForRepo(packageName: string, repo: string, registry = ''): Promise { if (packageName === '' || repo === '') return Promise.resolve(false) const base = registry === '' ? 'https://registry.npmjs.org' : registry.replace(/\/+$/, '') const url = `${base}/${encodeURIComponent(packageName)}/latest` @@ -90,9 +94,11 @@ export function isDshNpmPackageForRepo(packageName: string, repo: string, regist } let body = '' res.on('data', (chunk: Buffer) => { body += chunk.toString() }) - res.on('end', () => { + res.on('end', async () => { try { - resolve(isDshPackageMetadataForRepo(JSON.parse(body) as NpmSearchPackage, repo)) + const pkg = JSON.parse(body) as NpmSearchPackage + resolve(isDshPackageMetadataForRepo(pkg, repo) + || isDshPackageMetadataForRepo(pkg, await canonicalRepo(repo))) } catch { resolve(false) } @@ -103,6 +109,19 @@ export function isDshNpmPackageForRepo(packageName: string, repo: string, regist }) } +async function canonicalRepo(repo: string): Promise { + if (githubRepoOf(repo) === null) return repo + try { + const response = await fetch(`https://github.com/${repo}`, { + method: 'HEAD', + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }) + return response.ok ? githubRepoOf(response.url) ?? repo : repo + } catch { + return repo + } +} + function matchingPackagesForRepo(objects: NpmSearchObject[], repo: string): Array<{ name: string; isDshPlugin: boolean }> { const matches: Array<{ name: string; isDshPlugin: boolean }> = [] diff --git a/src/server/services/install/preflight.ts b/src/server/services/install/preflight.ts index 5fa4ca3..c82f739 100644 --- a/src/server/services/install/preflight.ts +++ b/src/server/services/install/preflight.ts @@ -59,7 +59,9 @@ export async function preflightTarget(target: string): Promise if (meta === null) return { ok: true, missing: null, name: null } const entry = resolvePackageEntry(meta) const inTar = await hasTarEntry(tar, `${rootPrefix}${entry}`) - return { ok: inTar, missing: inTar ? null : entry, name: typeof meta.name === 'string' ? meta.name : null } + const prepare = (meta.scripts as { prepare?: unknown } | undefined)?.prepare + const ok = inTar || (typeof prepare === 'string' && prepare.trim() !== '') + return { ok, missing: ok ? null : entry, name: typeof meta.name === 'string' ? meta.name : null } } catch { return { ok: true, missing: null, name: null } } finally {