verdaccio
Version:
A lightweight private npm proxy registry
210 lines (209 loc) • 8.23 kB
JavaScript
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