From b348db5ee9c68d3b3394f7d42a0475df22211e33 Mon Sep 17 00:00:00 2001 From: John Reilly Date: Sat, 7 Dec 2024 20:15:40 +0000 Subject: [PATCH] BREAKING CHANGE: registry feature and change shortcuts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## PR Checklist - [ ] Addresses an existing open issue: fixes #000 - [ ] That issue was marked as [`status: accepting prs`](https://github.com/johnnyreilly/azdo-npm-auth/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22) - [x] Steps in [CONTRIBUTING.md](https://github.com/johnnyreilly/azdo-npm-auth/blob/main/.github/CONTRIBUTING.md) were taken ## Overview This PR adds the `--registry` parameter / feature and changes the short alias for some other parameters. --- README.md | 29 +++----- src/bin/help.test.ts | 8 +- src/bin/index.ts | 73 ++++++++++++------- src/index.ts | 5 +- ...Npmrc.test.ts => projectNpmrcMake.test.ts} | 8 +- ...sedProjectNpmrc.ts => projectNpmrcMake.ts} | 2 +- ...pmrc.test.ts => projectNpmrcParse.test.ts} | 10 +-- ...seProjectNpmrc.ts => projectNpmrcParse.ts} | 28 ++----- src/projectNpmrcRegistry.test.ts | 27 +++++++ src/projectNpmrcRegistry.ts | 19 +++++ src/projectNpmrcShared.ts | 33 +++++++++ src/shared/options/args.ts | 16 +++- src/shared/options/optionsSchema.ts | 1 + 13 files changed, 177 insertions(+), 82 deletions(-) rename src/{makeParsedProjectNpmrc.test.ts => projectNpmrcMake.test.ts} (84%) rename src/{makeParsedProjectNpmrc.ts => projectNpmrcMake.ts} (96%) rename src/{parseProjectNpmrc.test.ts => projectNpmrcParse.test.ts} (89%) rename src/{parseProjectNpmrc.ts => projectNpmrcParse.ts} (51%) create mode 100644 src/projectNpmrcRegistry.test.ts create mode 100644 src/projectNpmrcRegistry.ts create mode 100644 src/projectNpmrcShared.ts diff --git a/README.md b/README.md index 8398778..29d2a2b 100644 --- a/README.md +++ b/README.md @@ -131,23 +131,18 @@ There is an official package named [`ado-npm-auth`](https://github.com/microsoft ## Options -`-c` | `--config` (`string`): The location of the .npmrc file. Defaults to current directory - -`-o` | `--organization` (`string`): The Azure DevOps organization - only required if not parsing from the .npmrc file - -`-r` | `--project` (`string`): The Azure DevOps project - only required if not parsing from the .npmrc file and the feed is project-scoped - -`-f` | `--feed` (`string`): The Azure Artifacts feed - only required if not parsing from the .npmrc file - -`-e` | `--email` (`string`): Allows users to supply an explicit email - if not supplied, the example ADO value will be used - -`-d` | `--daysToExpiry` (`number`): Allows users to supply an explicit number of days to expiry - if not supplied, then ADO will determine the expiry date - -`-p` | `--pat` (`string`): Allows users to supply an explicit Personal Access Token (which must include the Packaging read and write scopes) - if not supplied, will be acquired from the Azure CLI - -`-h` | `--help`: Show help - -`-v` | `--version`: Show version +| Short | Long | Type | Description | +| ----- | ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `-c` | `--config` | `string` | The location of the .npmrc file. Defaults to current directory | +| `-o` | `--organization` | `string` | The Azure DevOps organization - only required if not parsing from the .npmrc file | +| `-p` | `--project` | `string` | The Azure DevOps project - only required if not parsing from the .npmrc file and the feed is project-scoped | +| `-f` | `--feed` | `string` | The Azure Artifacts feed - only required if not parsing from the .npmrc file | +| `-r` | `--registry` | `string` | The registry to use, eg 'https://pkgs.dev.azure.com/johnnyreilly/_packaging/npmrc-script-organization/npm/registry/' - only required if not parsing from the .npmrc file | +| `-e` | `--email` | `string` | Allows users to supply an explicit email - if not supplied, the example ADO value will be used | +| `-d` | `--daysToExpiry` | `number` | Allows users to supply an explicit number of days to expiry - if not supplied, then ADO will determine the expiry date | +| `-t` | `--pat` | `string` | Allows users to supply an explicit Personal Access Token (which must include the Packaging read and write scopes) - if not supplied, will be acquired from the Azure CLI | +| `-h` | `--help` | | Show help | +| `-v` | `--version` | | Show version | ## Help with `npm error code E401` diff --git a/src/bin/help.test.ts b/src/bin/help.test.ts index f5e39f7..b90dc1f 100644 --- a/src/bin/help.test.ts +++ b/src/bin/help.test.ts @@ -60,12 +60,16 @@ describe("logHelpText", () => { ], [ " - -r | --project (string): The Azure DevOps project - only required if not parsing from the .npmrc file and the feed is project-scoped", + -p | --project (string): The Azure DevOps project - only required if not parsing from the .npmrc file and the feed is project-scoped", ], [ " -f | --feed (string): The Azure Artifacts feed - only required if not parsing from the .npmrc file", ], + [ + " + -r | --registry (string): The registry to use, eg 'https://pkgs.dev.azure.com/johnnyreilly/_packaging/npmrc-script-organization/npm/registry/' - only required if not parsing from the .npmrc file", + ], [ " -e | --email (string): Allows users to supply an explicit email - if not supplied, the example ADO value will be used", @@ -76,7 +80,7 @@ describe("logHelpText", () => { ], [ " - -p | --pat (string): Allows users to supply an explicit Personal Access Token (which must include the Packaging read and write scopes) - if not supplied, will be acquired from the Azure CLI", + -t | --pat (string): Allows users to supply an explicit Personal Access Token (which must include the Packaging read and write scopes) - if not supplied, will be acquired from the Azure CLI", ], [], ] diff --git a/src/bin/index.ts b/src/bin/index.ts index 813c80c..6cf6726 100644 --- a/src/bin/index.ts +++ b/src/bin/index.ts @@ -8,10 +8,11 @@ import { fromZodError } from "zod-validation-error"; import { createPat, createUserNpmrc, - makeParsedProjectNpmrc, - parseProjectNpmrc, + projectNpmrcMake, + projectNpmrcParse, writeNpmrc, } from "../index.js"; +import { projectNpmrcRegistry } from "../projectNpmrcRegistry.js"; import { withSpinner } from "../shared/cli/spinners.js"; import { StatusCodes } from "../shared/codes.js"; import { options } from "../shared/options/args.js"; @@ -63,6 +64,7 @@ export async function bin(args: string[]) { organization: values.organization, project: values.project, feed: values.feed, + registry: values.registry, email: values.email, daysToExpiry: values.daysToExpiry ? Number(values.daysToExpiry) : undefined, }; @@ -84,10 +86,18 @@ export async function bin(args: string[]) { return StatusCodes.Failure; } - const { config, organization, project, feed, email, pat, daysToExpiry } = - optionsParseResult.data; - - // TODO: this will prevent this file from running tests on the server after this - create an override parameter + const { + config, + organization, + project, + feed, + registry, + email, + pat, + daysToExpiry, + } = optionsParseResult.data; + + // TODO: this will prevent this file from running tests on the server after this - create an override parameter? if (ci.isCI) { logger.error( `Detected that you are running on a CI server (${ci.name ?? ""}) and so will not generate a user .npmrc file`, @@ -97,11 +107,18 @@ export async function bin(args: string[]) { return StatusCodes.Success; } - const shouldParseProjectNpmrc = !organization && !feed; - - const optionsSuffix = shouldParseProjectNpmrc - ? `- config: ${config ?? "[NONE SUPPLIED - WILL USE DEFAULT LOCATION]"}` - : `- organization: ${organization ?? ""} + const projectNpmrcMode = registry + ? "registry" + : !organization && !feed + ? "parse" + : "make"; + + const optionsSuffix = + projectNpmrcMode === "registry" + ? `- registry: ${registry ?? ""}` + : projectNpmrcMode === "parse" + ? `- config: ${config ?? "[NONE SUPPLIED - WILL USE DEFAULT LOCATION]"}` + : `- organization: ${organization ?? ""} - project: ${project ?? ""} - feed: ${feed ?? ""} `; @@ -116,23 +133,27 @@ ${optionsSuffix}`, try { const parsedProjectNpmrc = await withSpinner( - shouldParseProjectNpmrc - ? `Parsing project .npmrc` - : "Making parsed project .npmrc", + projectNpmrcMode === "registry" + ? `Using supplied registry` + : projectNpmrcMode === "parse" + ? `Parsing project .npmrc` + : "Making parsed project .npmrc", logger, async (logger) => { - return shouldParseProjectNpmrc - ? await parseProjectNpmrc({ - npmrcPath: config - ? path.resolve(config) - : path.resolve(process.cwd(), ".npmrc"), - logger, - }) - : makeParsedProjectNpmrc({ - organization: organization ?? "", - project, - feed: feed ?? "", - }); + return projectNpmrcMode === "registry" + ? projectNpmrcRegistry({ registry: registry ?? "", logger }) + : projectNpmrcMode === "parse" + ? await projectNpmrcParse({ + npmrcPath: config + ? path.resolve(config) + : path.resolve(process.cwd(), ".npmrc"), + logger, + }) + : projectNpmrcMake({ + organization: organization ?? "", + project, + feed: feed ?? "", + }); }, ); diff --git a/src/index.ts b/src/index.ts index 9f00c87..d93291c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export * from "./createPat.js"; export * from "./createUserNpmrc.js"; -export * from "./makeParsedProjectNpmrc.js"; -export * from "./parseProjectNpmrc.js"; +export * from "./projectNpmrcMake.js"; +export * from "./projectNpmrcParse.js"; +export * from "./projectNpmrcRegistry.js"; export * from "./types.js"; export * from "./writeNpmrc.js"; diff --git a/src/makeParsedProjectNpmrc.test.ts b/src/projectNpmrcMake.test.ts similarity index 84% rename from src/makeParsedProjectNpmrc.test.ts rename to src/projectNpmrcMake.test.ts index c836822..37c8067 100644 --- a/src/makeParsedProjectNpmrc.test.ts +++ b/src/projectNpmrcMake.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from "vitest"; -import { makeParsedProjectNpmrc } from "./makeParsedProjectNpmrc.js"; +import { projectNpmrcMake } from "./projectNpmrcMake.js"; -describe("makeParsedProjectNpmrc", () => { +describe("projectNpmrcMake", () => { it("given no project it constructs an organisation feed ParsedProjectNpmrc", () => { - const result = makeParsedProjectNpmrc({ + const result = projectNpmrcMake({ organization: "johnnyreilly", feed: "npmrc-script-organization", }); @@ -18,7 +18,7 @@ describe("makeParsedProjectNpmrc", () => { }); it("given a project it constructs a project feed ParsedProjectNpmrc", () => { - const result = makeParsedProjectNpmrc({ + const result = projectNpmrcMake({ organization: "johnnyreilly", project: "azure-static-web-apps", feed: "npmrc-script-demo", diff --git a/src/makeParsedProjectNpmrc.ts b/src/projectNpmrcMake.ts similarity index 96% rename from src/makeParsedProjectNpmrc.ts rename to src/projectNpmrcMake.ts index faf1e6c..05b718b 100644 --- a/src/makeParsedProjectNpmrc.ts +++ b/src/projectNpmrcMake.ts @@ -5,7 +5,7 @@ import { fallbackLogger, type Logger } from "./shared/cli/logger.js"; /** * Construct a ParsedProjectNpmrc object using the provided parameters */ -export function makeParsedProjectNpmrc({ +export function projectNpmrcMake({ organization, project, feed, diff --git a/src/parseProjectNpmrc.test.ts b/src/projectNpmrcParse.test.ts similarity index 89% rename from src/parseProjectNpmrc.test.ts rename to src/projectNpmrcParse.test.ts index 3d4f967..f160d89 100644 --- a/src/parseProjectNpmrc.test.ts +++ b/src/projectNpmrcParse.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import { parseProjectNpmrc } from "./parseProjectNpmrc.js"; +import { projectNpmrcParse } from "./projectNpmrcParse.js"; const mockReadFile = vi.fn(); @@ -10,12 +10,12 @@ vi.mock("./shared/readFileSafe.js", () => ({ }, })); -describe("parseProjectNpmrc", () => { +describe("projectNpmrcParse", () => { it("outputs the expected structure on successful parse", async () => { mockReadFile.mockResolvedValue(`registry=https://pkgs.dev.azure.com/johnnyreilly/_packaging/npmrc-script-organization/npm/registry/ always-auth=true`); - const result = await parseProjectNpmrc({ + const result = await projectNpmrcParse({ npmrcPath: "/home/john/code/github/azdo-npm-auth/.npmrc", }); expect(result).toEqual({ @@ -32,7 +32,7 @@ always-auth=true`); mockReadFile.mockResolvedValue(`registry=https://pkgs.dev.azure.com/johnnyreilly/_packaging/npmrc-script-organization/npm/registry always-auth=true`); - const result = await parseProjectNpmrc({ + const result = await projectNpmrcParse({ npmrcPath: "/home/john/code/github/azdo-npm-auth/.npmrc", }); expect(result).toEqual({ @@ -47,7 +47,7 @@ always-auth=true`); it("errors on invalid content", async () => { mockReadFile.mockResolvedValue(`stuff`); await expect(() => - parseProjectNpmrc({ + projectNpmrcParse({ npmrcPath: "/home/john/code/github/azdo-npm-auth/.npmrc", }), ).rejects.toThrowError("Unable to extract information from project .npmrc"); diff --git a/src/parseProjectNpmrc.ts b/src/projectNpmrcParse.ts similarity index 51% rename from src/parseProjectNpmrc.ts rename to src/projectNpmrcParse.ts index 28396b7..b89f9ac 100644 --- a/src/parseProjectNpmrc.ts +++ b/src/projectNpmrcParse.ts @@ -1,12 +1,13 @@ import type { ParsedProjectNpmrc } from "./types.js"; +import { makeFromRegistry } from "./projectNpmrcShared.js"; import { fallbackLogger, type Logger } from "./shared/cli/logger.js"; import { readFileSafe } from "./shared/readFileSafe.js"; /** * Read the project .npmrc file to acquire necessary info */ -export async function parseProjectNpmrc({ +export async function projectNpmrcParse({ npmrcPath, logger = fallbackLogger, }: { @@ -28,26 +29,7 @@ export async function parseProjectNpmrc({ throw new Error(`Unable to extract information from project .npmrc`); } - const urlWithoutRegistryAtStart = match[0] - .replace("registry=https:", "") - .trim(); - const urlWithoutRegistryAtEnd = urlWithoutRegistryAtStart.replace( - /registry\/$/, - "", - ); - // extract the organisation which we will use as the username - // not sure why this is the case, but this is the behaviour - // defined in ADO - const organisation = urlWithoutRegistryAtEnd.split("/")[3]; - - logger.info(`Parsed: -- organisation: ${organisation} -- urlWithoutRegistryAtStart: ${urlWithoutRegistryAtStart} -- urlWithoutRegistryAtEnd: ${urlWithoutRegistryAtEnd}`); - - return { - urlWithoutRegistryAtStart, - urlWithoutRegistryAtEnd, - organization: organisation, - }; + const registry = match[0].replace("registry=", "").trim(); + + return makeFromRegistry({ registry, logger }); } diff --git a/src/projectNpmrcRegistry.test.ts b/src/projectNpmrcRegistry.test.ts new file mode 100644 index 0000000..0c23a7c --- /dev/null +++ b/src/projectNpmrcRegistry.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; + +import { projectNpmrcRegistry } from "./projectNpmrcRegistry.js"; + +describe("projectNpmrcRegistry", () => { + it("outputs the expected structure on successful parse", () => { + const result = projectNpmrcRegistry({ + registry: + "https://pkgs.dev.azure.com/johnnyreilly/_packaging/npmrc-script-organization/npm/registry/", + }); + expect(result).toEqual({ + organization: "johnnyreilly", + urlWithoutRegistryAtEnd: + "//pkgs.dev.azure.com/johnnyreilly/_packaging/npmrc-script-organization/npm/", + urlWithoutRegistryAtStart: + "//pkgs.dev.azure.com/johnnyreilly/_packaging/npmrc-script-organization/npm/registry/", + }); + }); + + it("errors on invalid content", () => { + expect(() => + projectNpmrcRegistry({ + registry: "stuff", + }), + ).toThrowError("Unable to extract information"); + }); +}); diff --git a/src/projectNpmrcRegistry.ts b/src/projectNpmrcRegistry.ts new file mode 100644 index 0000000..b846a43 --- /dev/null +++ b/src/projectNpmrcRegistry.ts @@ -0,0 +1,19 @@ +import type { ParsedProjectNpmrc } from "./types.js"; + +import { makeFromRegistry } from "./projectNpmrcShared.js"; +import { fallbackLogger, type Logger } from "./shared/cli/logger.js"; + +/** + * Parse the registry parameter to acquire necessary info + */ +export function projectNpmrcRegistry({ + registry, + logger = fallbackLogger, +}: { + registry: string; + logger?: Logger; +}): ParsedProjectNpmrc { + logger.info(`Parsing from registry: ${registry}`); + + return makeFromRegistry({ registry, logger }); +} diff --git a/src/projectNpmrcShared.ts b/src/projectNpmrcShared.ts new file mode 100644 index 0000000..981b5f8 --- /dev/null +++ b/src/projectNpmrcShared.ts @@ -0,0 +1,33 @@ +import type { Logger } from "./shared/cli/logger.js"; + +export function makeFromRegistry({ + registry, + logger, +}: { + registry: string; + logger: Logger; +}) { + if (!registry.startsWith("https:")) { + throw new Error("Unable to extract information"); + } + const urlWithoutRegistryAtStart = registry.replace("https:", ""); + const urlWithoutRegistryAtEnd = urlWithoutRegistryAtStart.replace( + /registry\/$/, + "", + ); + // extract the organisation which we will use as the username + // not sure why this is the case, but this is the behaviour + // defined in ADO + const organisation = urlWithoutRegistryAtEnd.split("/")[3]; + + logger.info(`Parsed: +- organisation: ${organisation} +- urlWithoutRegistryAtStart: ${urlWithoutRegistryAtStart} +- urlWithoutRegistryAtEnd: ${urlWithoutRegistryAtEnd}`); + + return { + urlWithoutRegistryAtStart, + urlWithoutRegistryAtEnd, + organization: organisation, + }; +} diff --git a/src/shared/options/args.ts b/src/shared/options/args.ts index bdea89b..5c07767 100644 --- a/src/shared/options/args.ts +++ b/src/shared/options/args.ts @@ -10,7 +10,7 @@ export const options = { }, project: { - short: "r", + short: "p", type: "string", }, @@ -19,6 +19,11 @@ export const options = { type: "string", }, + registry: { + short: "r", + type: "string", + }, + email: { short: "e", type: "string", @@ -30,7 +35,7 @@ export const options = { }, pat: { - short: "p", + short: "t", type: "string", }, @@ -86,6 +91,13 @@ export const allArgOptions: Record = { docsSection: "optional", }, + registry: { + ...options.registry, + description: + "The registry to use, eg 'https://pkgs.dev.azure.com/johnnyreilly/_packaging/npmrc-script-organization/npm/registry/' - only required if not parsing from the .npmrc file", + docsSection: "optional", + }, + email: { ...options.email, description: diff --git a/src/shared/options/optionsSchema.ts b/src/shared/options/optionsSchema.ts index 653b31e..de15ad5 100644 --- a/src/shared/options/optionsSchema.ts +++ b/src/shared/options/optionsSchema.ts @@ -8,6 +8,7 @@ export const optionsSchema = z.object({ organization: z.string().optional(), project: z.string().optional(), feed: z.string().optional(), + registry: z.string().optional(), email: z.string().optional(), daysToExpiry: z.number().optional(),