diff --git a/package.json b/package.json index cf2fac43..1ec0e3a0 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ }, "dependencies": { "chalk": "^2.4.2", + "express": "^4.17.0", "global-agent": "^2.1.5", "got": "^9.6.0", "memory-cache": "^0.2.0", @@ -62,7 +63,6 @@ "babel-jest": "^24.9.0", "codecov": "^3.6.1", "core-js": "^3.3.4", - "express": "^4.17.0", "jest": "^24.9.0", "lodash": "4.17.15", "nodemon": "^1.18.10", @@ -80,7 +80,6 @@ "verdaccio-htpasswd": "^8.2.0" }, "peerDependencies": { - "express": "4", "lodash": "4", "verdaccio": "3 || 4" }, diff --git a/src/client/plugin/usage-info.ts b/src/client/plugin/usage-info.ts index 95595cb3..a6899d6c 100644 --- a/src/client/plugin/usage-info.ts +++ b/src/client/plugin/usage-info.ts @@ -9,7 +9,11 @@ export function getUsageInfo() { return "Click the login button to authenticate with GitHub." } - const configBase = "//" + location.host + location.pathname + const configBase = (window as any).VERDACCIO_API_URL + ? (window as any).VERDACCIO_API_URL + .replace(/^https?:/, "") + .replace(/-\/verdaccio\/$/, "") + : `//${location.host}${location.pathname}` const authToken = localStorage.getItem("npm") return [ `npm config set ${configBase}:_authToken "${authToken}"`, diff --git a/src/client/verdaccio-4.ts b/src/client/verdaccio-4.ts index 3d9e9d14..c901e507 100644 --- a/src/client/verdaccio-4.ts +++ b/src/client/verdaccio-4.ts @@ -1,7 +1,8 @@ import { getUsageInfo, init, isLoggedIn } from "./plugin" -const usageInfoSelector = "#help-card .MuiCardContent-root span, .MuiDialogContent-root .MuiTypography-root span" -const randomId = "Os1waV6BSoZQKfFwNlIwS" +const helpCardUsageInfoSelector = "#help-card .MuiCardContent-root span" +const dialogUsageInfoSelector = "#registryInfo--dialog-container .MuiDialogContent-root .MuiTypography-root span" +const randomClass = "Os1waV6BSoZQKfFwNlIwS" // copied from here as it needs to be the same behaviour // https://github.com/verdaccio/ui/blob/master/src/utils/cli-utils.ts @@ -18,39 +19,67 @@ export function copyToClipboard(text: string) { document.body.removeChild(node) } -function markUsageInfoNodes() { - document.querySelectorAll(usageInfoSelector).forEach(node => { - const infoEl = node as HTMLSpanElement - - if (infoEl.innerText.includes("adduser")) { - infoEl.classList.add(randomId) - } - }) -} - -function modifyUsageInfoNodes() { +function modifyUsageInfoNodes( + selector: string, + findPredicate: (node: HTMLElement) => boolean, +): void { const usageInfo = getUsageInfo() const loggedIn = isLoggedIn() - document.querySelectorAll("." + randomId).forEach(node => { - const infoEl = node as HTMLSpanElement - const parentEl = infoEl.parentElement as HTMLDivElement - const copyEl = parentEl.querySelector("button") as HTMLButtonElement + const infoElements: NodeListOf = document.querySelectorAll(selector) + const firstUsageInfoEl = Array.prototype.find.call(infoElements, findPredicate) + const hasInjectedElement = !!Array.prototype.find.call( + infoElements, + (node: HTMLElement) => node.parentElement!.classList.contains(randomClass), + ) + + // We can't find any element related to usage instructions, + // or we have already injected elements + if (!firstUsageInfoEl || hasInjectedElement) { + return + } + + const cachedParent: HTMLDivElement | null = firstUsageInfoEl.parentElement + if (cachedParent) { + usageInfo.split("\n").reverse().forEach(info => { + const clonedNode = cachedParent.cloneNode(true) as HTMLDivElement + const textElem = clonedNode.querySelector("span")! + const copyEl = clonedNode.querySelector("button")! + + clonedNode.classList.add(randomClass) + textElem.innerText = info + copyEl.style.visibility = loggedIn ? "visible" : "hidden" + copyEl.onclick = e => { + e.preventDefault() + e.stopPropagation() + copyToClipboard(info) + } - infoEl.innerText = usageInfo + cachedParent.insertAdjacentElement("afterend", clonedNode) + }) + } - copyEl.style.visibility = loggedIn ? "visible" : "hidden" - copyEl.onclick = e => { - e.preventDefault() - e.stopPropagation() - copyToClipboard(usageInfo) + infoElements.forEach((node => { + if ( + // We only match lines related to bundler commands + !!node.innerText.match(/^(npm|pnpm|yarn)/) && + // And only commands that we want to remove + (node.innerText.includes("adduser") || node.innerText.includes("set password")) + ) { + node.parentElement!.parentElement!.removeChild(node.parentElement!) } - }) + })) } function updateUsageInfo() { - markUsageInfoNodes() - modifyUsageInfoNodes() + modifyUsageInfoNodes(helpCardUsageInfoSelector, node => node.innerText.includes("adduser")) + modifyUsageInfoNodes( + dialogUsageInfoSelector, + node => !!node.innerText.match( + // This checks for an element showing instructions to set the registry URL + /((npm|pnpm) set|(yarn) config set)/, + ) + ) } init({ diff --git a/src/server/flows/WebFlow.ts b/src/server/flows/WebFlow.ts index 2fb0e92d..ba247bcc 100644 --- a/src/server/flows/WebFlow.ts +++ b/src/server/flows/WebFlow.ts @@ -60,7 +60,7 @@ export class WebFlow implements IPluginMiddleware { * We issue a JWT using these values and pass them back to the frontend as * query parameters so they can be stored in the browser. * - * The username and token are encryped and base64 encoded to form a token for + * The username and token are encrypted and base64 encoded to form a token for * the npm CLI. * * There is no need to later decode and decrypt the token. This process is