UNPKG

verdaccio

Version:

A lightweight private npm proxy registry

210 lines (209 loc) • 8.23 kB
import createDebug from "debug"; import { Writable } from "node:stream"; //#region src/lib/legacy-storage-adapter.ts var debug = createDebug("verdaccio:storage:legacy-adapter"); /** Resolve an error-first callback method into a promise. */ function fromCallback(invoke) { return new Promise((resolve, reject) => { invoke((err, data) => err ? reject(err) : resolve(data)); }); } /** * Detect a legacy callback-based storage plugin. The new contract's `get()` * takes no arguments (returns a promise); the legacy `get(cb)` declares a * trailing callback, and `add(name, cb)` takes two. Arity is a stable signal * here because both contracts are frozen. */ function isLegacyStoragePlugin(plugin) { if (!plugin || typeof plugin.get !== "function") return false; const getIsCallback = plugin.get.length >= 1; const addIsCallback = typeof plugin.add === "function" && plugin.add.length >= 2; return getIsCallback || addIsCallback; } /** Synthesize the new `hasPackage()` from legacy `readPackage(name, cb)`. */ function hasPackage(handler, pkgName) { return new Promise((resolve) => { handler.readPackage(pkgName, (err) => resolve(!err)); }); } /** * Synthesize the new `hasTarball(file)` from a legacy `readTarball` stream: * open => exists, 404/ENOENT error => missing. Mirrors the old * `Storage.hasLocalTarball` probe. */ function hasTarball(handler, fileName) { return new Promise((resolve, reject) => { let stream = handler.readTarball(fileName); let isOpen = false; stream.on("error", (err) => { const notFound = err?.code === "ENOENT" || err?.status === 404; if (isOpen || !notFound) { if (!isOpen) resolve(false); else reject(err); } else resolve(false); stream?.abort?.(); stream = null; }); stream.on("open", () => { isOpen = true; stream.abort?.(); stream = null; resolve(true); }); }); } /** * Bridge legacy `readTarball(name) => ReadTarball` to the promise contract. * The legacy object is already a Readable that emits `content-length`/`open`/ * `error`, so it can be handed back as-is; we only wire abort to the signal. */ function readTarball(handler, fileName, options = {}) { const stream = handler.readTarball(fileName); const { signal } = options; if (signal) { if (signal.aborted) stream.abort?.(); else signal.addEventListener("abort", () => stream.abort?.(), { once: true }); } return Promise.resolve(stream); } /** * Bridge legacy `writeTarball(name) => UploadTarball` to a Node Writable that * `@verdaccio/store` can use in `pipeline()`: * - the legacy stream finalizes only when `.done()` is called (ends temp file, * renames it into place, then emits `success`); store instead ends the * stream and waits for `close`. * - so we forward writes to the legacy stream, call `.done()` on `_final`, and * only complete (emit `close`) once the legacy stream emits `success`. */ function writeTarball(handler, fileName, options = {}) { const upload = handler.writeTarball(fileName); const { signal } = options; const bridge = new Writable({ write(chunk, _enc, cb) { if (upload.write(chunk)) cb(); else upload.once("drain", cb); }, final(cb) { upload.once("success", () => cb()); upload.once("error", (err) => cb(err)); upload.end(); upload.done(); } }); upload.on("open", () => bridge.emit("open")); upload.on("error", (err) => bridge.destroy(err)); if (signal) { const onAbort = () => { upload.abort?.(); bridge.destroy(/* @__PURE__ */ new Error("tarball upload aborted")); }; if (signal.aborted) onAbort(); else signal.addEventListener("abort", onAbort, { once: true }); } return Promise.resolve(bridge); } /** * Bridge legacy 5-arg `updatePackage(name, updateHandler, onWrite, * transformPackage, onEnd)` to the new 2-arg `updatePackage(name, handleUpdate)` * where `handleUpdate(manifest) => Promise<manifest>`. The store persists the * result itself (via `writePackage`/`savePackage`), so `onWrite` is a no-op: * we only read-lock, run the handler, and unlock. */ function updatePackage(handler, name, handleUpdate) { return new Promise((resolve, reject) => { let updated; handler.updatePackage(name, (json, cb) => { Promise.resolve(handleUpdate(json)).then((result) => { updated = result; cb(); }).catch(cb); }, (_name, _data, cb) => cb(), (json) => updated ?? json, (err) => err ? reject(err) : resolve(updated)); }); } /** Wrap a legacy per-package storage handler in the promise contract. */ function wrapLegacyHandler(handler) { if (!handler) return handler; return { readPackage: (name) => fromCallback((cb) => handler.readPackage(name, cb)), createPackage: (name, manifest) => fromCallback((cb) => handler.createPackage(name, manifest, cb)), savePackage: (name, value) => fromCallback((cb) => handler.savePackage(name, value, cb)), deletePackage: (fileName) => fromCallback((cb) => handler.deletePackage(fileName, cb)), removePackage: () => fromCallback((cb) => handler.removePackage(cb)), hasPackage: (name) => hasPackage(handler, name), hasTarball: (fileName) => hasTarball(handler, fileName), readTarball: (fileName, options) => readTarball(handler, fileName, options), writeTarball: (fileName, options) => writeTarball(handler, fileName, options), updatePackage: (name, handleUpdate) => updatePackage(handler, name, handleUpdate) }; } /** * The legacy contract emits the package itself, while `@verdaccio/store` reads * `item.package.name` and `item.score`. Wrap whatever is not already in that shape, * otherwise a plugin following the documented contract makes `/-/v1/search` fail. */ function toSearchItem(item) { if (item !== null && typeof item === "object" && "package" in item) return item; return { package: item, score: { final: 1, detail: { quality: 1, popularity: 1, maintenance: 1 } } }; } /** * Collect a legacy streaming `search(onPackage, onEnd, validate)` into a list. * * A legacy plugin has no way to receive the query, so the filtering happens here, on * the emitted name. The predicate stays permissive on purpose: a plugin calls it with * the basename (`pkg` for `@scope/pkg`), so any rejection there could drop a package * whose full name does match. */ function search(plugin, query) { const text = typeof query?.text === "string" ? query.text.toLowerCase() : void 0; const matches = (name) => text === void 0 || typeof name === "string" && name.toLowerCase().includes(text); return new Promise((resolve, reject) => { const items = []; try { plugin.search((item, done) => { const searchItem = toSearchItem(item); if (matches(searchItem?.package?.name)) items.push(searchItem); done(); }, (err) => err ? reject(err) : resolve(items), () => true); } catch (err) { reject(err); } }); } /** * Wrap a legacy callback storage plugin in the promise contract expected by * `@verdaccio/store`. Token methods are exposed only when the legacy plugin * implements them (store already guards their absence with SERVICE_UNAVAILABLE). */ function wrapLegacyStoragePlugin(plugin, logger) { debug("wrapping legacy callback storage plugin"); logger.warn("a legacy callback-based storage plugin was detected and wrapped for compatibility; consider upgrading it to the promise-based storage API"); const wrapped = { init: () => typeof plugin.init === "function" ? Promise.resolve(plugin.init()) : Promise.resolve(void 0), getSecret: () => Promise.resolve(plugin.getSecret()), setSecret: (secret) => Promise.resolve(plugin.setSecret(secret)), get: () => fromCallback((cb) => plugin.get(cb)), add: (name) => fromCallback((cb) => plugin.add(name, cb)), remove: (name) => fromCallback((cb) => plugin.remove(name, cb)), search: (query) => search(plugin, query), getPackageStorage: (packageName) => wrapLegacyHandler(plugin.getPackageStorage(packageName)) }; for (const tokenMethod of [ "saveToken", "deleteToken", "readTokens" ]) if (typeof plugin[tokenMethod] === "function") wrapped[tokenMethod] = (...args) => Promise.resolve(plugin[tokenMethod](...args)); return Object.assign(Object.create(plugin), wrapped); } //#endregion export { isLegacyStoragePlugin, wrapLegacyStoragePlugin }; //# sourceMappingURL=legacy-storage-adapter.mjs.map