diff --git a/docs/lib/build.js b/docs/lib/build.js index 86f8acac102f1..4838c8f487808 100644 --- a/docs/lib/build.js +++ b/docs/lib/build.js @@ -7,6 +7,109 @@ const parseFrontMatter = require('front-matter') const checkNav = require('./check-nav.js') const { DOC_EXT, ...transform } = require('./index.js') +// Auto-generate doc templates for commands without docs +const autoGenerateMissingDocs = async (contentPath, navPath) => { + const commandsPath = join(__dirname, '../../lib/commands') + const docsCommandsPath = join(contentPath, 'commands') + + // Get all command files + const commandFiles = await fs.readdir(commandsPath) + const commands = commandFiles + .filter(f => f.endsWith('.js')) + .map(f => basename(f, '.js')) + + // Get existing doc files + const existingDocs = await fs.readdir(docsCommandsPath) + const documentedCommands = existingDocs + .filter(f => f.startsWith('npm-') && f.endsWith(DOC_EXT)) + .map(f => f.replace('npm-', '').replace(DOC_EXT, '')) + + // Find commands without docs + const missingDocs = commands.filter(cmd => !documentedCommands.includes(cmd)) + + // Generate docs for missing commands + for (const cmd of missingDocs) { + const Command = require(join(commandsPath, `${cmd}.js`)) + const description = Command.description || `The ${cmd} command` + const docPath = join(docsCommandsPath, `npm-${cmd}${DOC_EXT}`) + + const template = `--- +title: npm-${cmd} +section: 1 +description: ${description} +--- + +### Synopsis + + + +### Description + +${description} + +### Configuration + + + +### See Also + +* [npm help config](/commands/npm-config) +` + + await fs.writeFile(docPath, template, 'utf-8') + } + + // Update nav.yml if there are new commands + if (missingDocs.length > 0) { + const navContent = await fs.readFile(navPath, 'utf-8') + const navData = yaml.parse(navContent) + + // Find the CLI Commands section + const commandsSection = navData.find(section => section.title === 'CLI Commands') + if (commandsSection && commandsSection.children) { + // Get existing command entries + const existingEntries = new Set( + commandsSection.children + .map(child => child.url?.replace('/commands/npm-', '')) + .filter(Boolean) + ) + + // Add missing commands to the children array + for (const cmd of missingDocs) { + if (!existingEntries.has(cmd)) { + const Command = require(join(commandsPath, `${cmd}.js`)) + const description = Command.description || `The ${cmd} command` + + commandsSection.children.push({ + title: `npm ${cmd}`, + url: `/commands/npm-${cmd}`, + description: description, + }) + } + } + + // Sort children alphabetically by title + commandsSection.children.sort((a, b) => { + if (a.title === 'npm') { + return -1 + } + if (b.title === 'npm') { + return 1 + } + return a.title.localeCompare(b.title) + }) + + // Write updated nav.yml + const prefix = ` +# This is the navigation for the documentation pages; it is not used +# directly within the CLI documentation. Instead, it will be used +# for the https://docs.npmjs.com/ site. +` + await fs.writeFile(navPath, `${prefix}\n\n${yaml.stringify(navData)}`, 'utf-8') + } + } +} + const mkDirs = async (paths) => { const uniqDirs = [...new Set(paths.map((p) => dirname(p)))] return Promise.all(uniqDirs.map((d) => fs.mkdir(d, { recursive: true }))) @@ -29,6 +132,9 @@ const pAll = async (obj) => { } const run = async ({ content, template, nav, man, html, md }) => { + // Auto-generate docs for commands without documentation + await autoGenerateMissingDocs(content, nav) + await rmAll(man, html, md) const [contentPaths, navFile, options] = await Promise.all([ readDocs(content), diff --git a/docs/lib/content/commands/npm-get.md b/docs/lib/content/commands/npm-get.md new file mode 100644 index 0000000000000..9e03458e7c8ce --- /dev/null +++ b/docs/lib/content/commands/npm-get.md @@ -0,0 +1,21 @@ +--- +title: npm-get +section: 1 +description: Get a value from the npm configuration +--- + +### Synopsis + + + +### Description + +Get a value from the npm configuration + +### Configuration + + + +### See Also + +* [npm help config](/commands/npm-config) diff --git a/docs/lib/content/commands/npm-ll.md b/docs/lib/content/commands/npm-ll.md new file mode 100644 index 0000000000000..cceb4284592ef --- /dev/null +++ b/docs/lib/content/commands/npm-ll.md @@ -0,0 +1,21 @@ +--- +title: npm-ll +section: 1 +description: List installed packages +--- + +### Synopsis + + + +### Description + +List installed packages + +### Configuration + + + +### See Also + +* [npm help config](/commands/npm-config) diff --git a/docs/lib/content/commands/npm-set.md b/docs/lib/content/commands/npm-set.md new file mode 100644 index 0000000000000..864ce81be43ba --- /dev/null +++ b/docs/lib/content/commands/npm-set.md @@ -0,0 +1,21 @@ +--- +title: npm-set +section: 1 +description: Set a value in the npm configuration +--- + +### Synopsis + + + +### Description + +Set a value in the npm configuration + +### Configuration + + + +### See Also + +* [npm help config](/commands/npm-config) diff --git a/docs/lib/content/nav.yml b/docs/lib/content/nav.yml index f6f8014f28071..f3a1bf7779f3a 100644 --- a/docs/lib/content/nav.yml +++ b/docs/lib/content/nav.yml @@ -1,267 +1,276 @@ + # This is the navigation for the documentation pages; it is not used # directly within the CLI documentation. Instead, it will be used # for the https://docs.npmjs.com/ site. + - title: CLI Commands shortName: Commands url: /commands children: - - title: npm - url: /commands/npm - description: JavaScript package manager - - title: npm access - url: /commands/npm-access - description: Set access level on published packages - - title: npm adduser - url: /commands/npm-adduser - description: Add a registry user account - - title: npm audit - url: /commands/npm-audit - description: Run a security audit - - title: npm bugs - url: /commands/npm-bugs - description: Bugs for a package in a web browser maybe - - title: npm cache - url: /commands/npm-cache - description: Manipulates packages cache - - title: npm ci - url: /commands/npm-ci - description: Install a project with a clean slate - - title: npm completion - url: /commands/npm-completion - description: Tab completion for npm - - title: npm config - url: /commands/npm-config - description: Manage the npm configuration files - - title: npm dedupe - url: /commands/npm-dedupe - description: Reduce duplication - - title: npm deprecate - url: /commands/npm-deprecate - description: Deprecate a version of a package - - title: npm diff - url: /commands/npm-diff - description: The registry diff command - - title: npm dist-tag - url: /commands/npm-dist-tag - description: Modify package distribution tags - - title: npm docs - url: /commands/npm-docs - description: Docs for a package in a web browser maybe - - title: npm doctor - url: /commands/npm-doctor - description: Check your environments - - title: npm edit - url: /commands/npm-edit - description: Edit an installed package - - title: npm exec - url: /commands/npm-exec - description: Run a command from an npm package - - title: npm explain - url: /commands/npm-explain - description: Explain installed packages - - title: npm explore - url: /commands/npm-explore - description: Browse an installed package - - title: npm find-dupes - url: /commands/npm-find-dupes - description: Find duplication in the package tree - - title: npm fund - url: /commands/npm-fund - description: Retrieve funding information - - title: npm help - url: /commands/npm-help - description: Search npm help documentation - - title: npm help-search - url: /commands/npm-help-search - description: Get help on npm - - title: npm init - url: /commands/npm-init - description: Create a package.json file - - title: npm install - url: /commands/npm-install - description: Install a package - - title: npm install-ci-test - url: /commands/npm-install-ci-test - description: Install a project with a clean slate and run tests - - title: npm install-test - url: /commands/npm-install-test - description: Install package(s) and run tests - - title: npm link - url: /commands/npm-link - description: Symlink a package folder - - title: npm login - url: /commands/npm-login - description: Login to a registry user account - - title: npm logout - url: /commands/npm-logout - description: Log out of the registry - - title: npm ls - url: /commands/npm-ls - description: List installed packages - - title: npm org - url: /commands/npm-org - description: Manage orgs - - title: npm outdated - url: /commands/npm-outdated - description: Check for outdated packages - - title: npm owner - url: /commands/npm-owner - description: Manage package owners - - title: npm pack - url: /commands/npm-pack - description: Create a tarball from a package - - title: npm ping - url: /commands/npm-ping - description: Ping npm registry - - title: npm pkg - url: /commands/npm-pkg - description: Manages your package.json - - title: npm prefix - url: /commands/npm-prefix - description: Display prefix - - title: npm profile - url: /commands/npm-profile - description: Change settings on your registry profile - - title: npm prune - url: /commands/npm-prune - description: Remove extraneous packages - - title: npm publish - url: /commands/npm-publish - description: Publish a package - - title: npm query - url: /commands/npm-query - description: Retrieve a filtered list of packages - - title: npm rebuild - url: /commands/npm-rebuild - description: Rebuild a package - - title: npm repo - url: /commands/npm-repo - description: Open package repository page in the browser - - title: npm restart - url: /commands/npm-restart - description: Restart a package - - title: npm root - url: /commands/npm-root - description: Display npm root - - title: npm run - url: /commands/npm-run - description: Run arbitrary package scripts - - title: npm sbom - url: /commands/npm-sbom - description: Generate a Software Bill of Materials (SBOM) - - title: npm search - url: /commands/npm-search - description: Search for packages - - title: npm shrinkwrap - url: /commands/npm-shrinkwrap - description: Lock down dependency versions for publication - - title: npm star - url: /commands/npm-star - description: Mark your favorite packages - - title: npm stars - url: /commands/npm-stars - description: View packages marked as favorites - - title: npm start - url: /commands/npm-start - description: Start a package - - title: npm stop - url: /commands/npm-stop - description: Stop a package - - title: npm team - url: /commands/npm-team - description: Manage organization teams and team memberships - - title: npm test - url: /commands/npm-test - description: Test a package - - title: npm token - url: /commands/npm-token - description: Manage your authentication tokens - - title: npm undeprecate - url: /commands/npm-undeprecate - description: Undeprecate a version of a package - - title: npm uninstall - url: /commands/npm-uninstall - description: Remove a package - - title: npm unpublish - url: /commands/npm-unpublish - description: Remove a package from the registry - - title: npm unstar - url: /commands/npm-unstar - description: Remove an item from your favorite packages - - title: npm update - url: /commands/npm-update - description: Update a package - - title: npm version - url: /commands/npm-version - description: Bump a package version - - title: npm view - url: /commands/npm-view - description: View registry info - - title: npm whoami - url: /commands/npm-whoami - description: Display npm username - - title: npx - url: /commands/npx - description: Run a command from an npm package - + - title: npm + url: /commands/npm + description: JavaScript package manager + - title: npm access + url: /commands/npm-access + description: Set access level on published packages + - title: npm adduser + url: /commands/npm-adduser + description: Add a registry user account + - title: npm audit + url: /commands/npm-audit + description: Run a security audit + - title: npm bugs + url: /commands/npm-bugs + description: Bugs for a package in a web browser maybe + - title: npm cache + url: /commands/npm-cache + description: Manipulates packages cache + - title: npm ci + url: /commands/npm-ci + description: Install a project with a clean slate + - title: npm completion + url: /commands/npm-completion + description: Tab completion for npm + - title: npm config + url: /commands/npm-config + description: Manage the npm configuration files + - title: npm dedupe + url: /commands/npm-dedupe + description: Reduce duplication + - title: npm deprecate + url: /commands/npm-deprecate + description: Deprecate a version of a package + - title: npm diff + url: /commands/npm-diff + description: The registry diff command + - title: npm dist-tag + url: /commands/npm-dist-tag + description: Modify package distribution tags + - title: npm docs + url: /commands/npm-docs + description: Docs for a package in a web browser maybe + - title: npm doctor + url: /commands/npm-doctor + description: Check your environments + - title: npm edit + url: /commands/npm-edit + description: Edit an installed package + - title: npm exec + url: /commands/npm-exec + description: Run a command from an npm package + - title: npm explain + url: /commands/npm-explain + description: Explain installed packages + - title: npm explore + url: /commands/npm-explore + description: Browse an installed package + - title: npm find-dupes + url: /commands/npm-find-dupes + description: Find duplication in the package tree + - title: npm fund + url: /commands/npm-fund + description: Retrieve funding information + - title: npm get + url: /commands/npm-get + description: Get a value from the npm configuration + - title: npm help + url: /commands/npm-help + description: Search npm help documentation + - title: npm help-search + url: /commands/npm-help-search + description: Get help on npm + - title: npm init + url: /commands/npm-init + description: Create a package.json file + - title: npm install + url: /commands/npm-install + description: Install a package + - title: npm install-ci-test + url: /commands/npm-install-ci-test + description: Install a project with a clean slate and run tests + - title: npm install-test + url: /commands/npm-install-test + description: Install package(s) and run tests + - title: npm link + url: /commands/npm-link + description: Symlink a package folder + - title: npm ll + url: /commands/npm-ll + description: List installed packages + - title: npm login + url: /commands/npm-login + description: Login to a registry user account + - title: npm logout + url: /commands/npm-logout + description: Log out of the registry + - title: npm ls + url: /commands/npm-ls + description: List installed packages + - title: npm org + url: /commands/npm-org + description: Manage orgs + - title: npm outdated + url: /commands/npm-outdated + description: Check for outdated packages + - title: npm owner + url: /commands/npm-owner + description: Manage package owners + - title: npm pack + url: /commands/npm-pack + description: Create a tarball from a package + - title: npm ping + url: /commands/npm-ping + description: Ping npm registry + - title: npm pkg + url: /commands/npm-pkg + description: Manages your package.json + - title: npm prefix + url: /commands/npm-prefix + description: Display prefix + - title: npm profile + url: /commands/npm-profile + description: Change settings on your registry profile + - title: npm prune + url: /commands/npm-prune + description: Remove extraneous packages + - title: npm publish + url: /commands/npm-publish + description: Publish a package + - title: npm query + url: /commands/npm-query + description: Retrieve a filtered list of packages + - title: npm rebuild + url: /commands/npm-rebuild + description: Rebuild a package + - title: npm repo + url: /commands/npm-repo + description: Open package repository page in the browser + - title: npm restart + url: /commands/npm-restart + description: Restart a package + - title: npm root + url: /commands/npm-root + description: Display npm root + - title: npm run + url: /commands/npm-run + description: Run arbitrary package scripts + - title: npm sbom + url: /commands/npm-sbom + description: Generate a Software Bill of Materials (SBOM) + - title: npm search + url: /commands/npm-search + description: Search for packages + - title: npm set + url: /commands/npm-set + description: Set a value in the npm configuration + - title: npm shrinkwrap + url: /commands/npm-shrinkwrap + description: Lock down dependency versions for publication + - title: npm star + url: /commands/npm-star + description: Mark your favorite packages + - title: npm stars + url: /commands/npm-stars + description: View packages marked as favorites + - title: npm start + url: /commands/npm-start + description: Start a package + - title: npm stop + url: /commands/npm-stop + description: Stop a package + - title: npm team + url: /commands/npm-team + description: Manage organization teams and team memberships + - title: npm test + url: /commands/npm-test + description: Test a package + - title: npm token + url: /commands/npm-token + description: Manage your authentication tokens + - title: npm undeprecate + url: /commands/npm-undeprecate + description: Undeprecate a version of a package + - title: npm uninstall + url: /commands/npm-uninstall + description: Remove a package + - title: npm unpublish + url: /commands/npm-unpublish + description: Remove a package from the registry + - title: npm unstar + url: /commands/npm-unstar + description: Remove an item from your favorite packages + - title: npm update + url: /commands/npm-update + description: Update a package + - title: npm version + url: /commands/npm-version + description: Bump a package version + - title: npm view + url: /commands/npm-view + description: View registry info + - title: npm whoami + url: /commands/npm-whoami + description: Display npm username + - title: npx + url: /commands/npx + description: Run a command from an npm package - title: Configuring npm shortName: Configuring url: /configuring-npm children: - - title: Install - url: /configuring-npm/install - description: Download and install node and npm - - title: Folders - url: /configuring-npm/folders - description: Folder structures used by npm - - title: .npmrc - url: /configuring-npm/npmrc - description: The npm config files - - title: npm-shrinkwrap.json - url: /configuring-npm/npm-shrinkwrap-json - description: A publishable lockfile - - title: package.json - url: /configuring-npm/package-json - description: Specifics of npm's package.json handling - - title: package-lock.json - url: /configuring-npm/package-lock-json - description: A manifestation of the manifest - + - title: Install + url: /configuring-npm/install + description: Download and install node and npm + - title: Folders + url: /configuring-npm/folders + description: Folder structures used by npm + - title: .npmrc + url: /configuring-npm/npmrc + description: The npm config files + - title: npm-shrinkwrap.json + url: /configuring-npm/npm-shrinkwrap-json + description: A publishable lockfile + - title: package.json + url: /configuring-npm/package-json + description: Specifics of npm's package.json handling + - title: package-lock.json + url: /configuring-npm/package-lock-json + description: A manifestation of the manifest - title: Using npm shortName: Using url: /using-npm children: - - title: Registry - url: /using-npm/registry - description: The JavaScript Package Registry - - title: Package spec - url: /using-npm/package-spec - description: Package name specifier - - title: Config - url: /using-npm/config - description: About npm configuration - - title: Logging - url: /using-npm/logging - description: Why, What & How we Log - - title: Scope - url: /using-npm/scope - description: Scoped packages - - title: Scripts - url: /using-npm/scripts - description: How npm handles the "scripts" field - - title: Workspaces - url: /using-npm/workspaces - description: Working with workspaces - - title: Organizations - url: /using-npm/orgs - description: Working with teams & organizations - - title: Dependency Selectors - url: /using-npm/dependency-selectors - description: Dependency Selector Syntax & Querying - - title: Developers - url: /using-npm/developers - description: Developer guide - - title: Removal - url: /using-npm/removal - description: Cleaning the slate + - title: Registry + url: /using-npm/registry + description: The JavaScript Package Registry + - title: Package spec + url: /using-npm/package-spec + description: Package name specifier + - title: Config + url: /using-npm/config + description: About npm configuration + - title: Logging + url: /using-npm/logging + description: Why, What & How we Log + - title: Scope + url: /using-npm/scope + description: Scoped packages + - title: Scripts + url: /using-npm/scripts + description: How npm handles the "scripts" field + - title: Workspaces + url: /using-npm/workspaces + description: Working with workspaces + - title: Organizations + url: /using-npm/orgs + description: Working with teams & organizations + - title: Dependency Selectors + url: /using-npm/dependency-selectors + description: Dependency Selector Syntax & Querying + - title: Developers + url: /using-npm/developers + description: Developer guide + - title: Removal + url: /using-npm/removal + description: Cleaning the slate diff --git a/docs/lib/index.js b/docs/lib/index.js index 5e40f48882cad..d9565c4f06757 100644 --- a/docs/lib/index.js +++ b/docs/lib/index.js @@ -40,12 +40,17 @@ const getCommandByDoc = (docFile, docExt) => { // `npx` is not technically a command in and of itself, // so it just needs the usage of npm exec const srcName = name === 'npx' ? 'exec' : name - const { params, usage = [''], workspaces } = require(`../../lib/commands/${srcName}`) + const command = require(`../../lib/commands/${srcName}`) + const { params, usage = [''], workspaces } = command + const commandDefinitions = command.definitions || {} + const definitionPool = { ...definitions, ...commandDefinitions } const usagePrefix = name === 'npx' ? 'npx' : `npm ${name}` if (params) { for (const param of params) { - if (definitions[param].exclusive) { - for (const e of definitions[param].exclusive) { + // Check command-specific definitions first, fall back to global definitions + const paramDef = definitionPool[param] + if (paramDef && paramDef.exclusive) { + for (const e of paramDef.exclusive) { if (!params.includes(e)) { params.splice(params.indexOf(param) + 1, 0, e) } @@ -93,14 +98,30 @@ const replaceUsage = (src, { path }) => { } const replaceParams = (src, { path }) => { - const { params } = getCommandByDoc(path, DOC_EXT) + const { params, name } = getCommandByDoc(path, DOC_EXT) const replacer = params && assertPlaceholder(src, path, TAGS.CONFIG) if (!params) { return src } - const paramsConfig = params.map((n) => definitions[n].describe()) + // Load command to get command-specific definitions if they exist + let commandDefinitions = {} + if (name && name !== 'npm' && name !== 'npx') { + try { + const srcName = name === 'npx' ? 'exec' : name + const command = require(`../../lib/commands/${srcName}`) + commandDefinitions = command.definitions || {} + } catch { + // If command doesn't exist or has no definitions, continue with global definitions only + } + } + + const paramsConfig = params.map((n) => { + // Check command-specific definitions first, fall back to global definitions + const def = commandDefinitions[n] || definitions[n] + return def.describe() + }) return src.replace(replacer, paramsConfig.join('\n\n')) } diff --git a/lib/base-cmd.js b/lib/base-cmd.js index 3e6c4758cbd58..7326e160a6fee 100644 --- a/lib/base-cmd.js +++ b/lib/base-cmd.js @@ -1,4 +1,5 @@ const { log } = require('proc-log') +const { definitions } = require('@npmcli/config/lib/definitions') class BaseCommand { // these defaults can be overridden by individual commands @@ -10,16 +11,24 @@ class BaseCommand { static name = null static description = null static params = null + static definitions = null // this is a static so that we can read from it without instantiating a command // which would require loading the config static get describeUsage () { - const { definitions } = require('@npmcli/config/lib/definitions') const { aliases: cmdAliases } = require('./utils/cmd-list') const seenExclusive = new Set() const wrapWidth = 80 const { description, usage = [''], name, params } = this + let definitionsPool = {} + if (this.definitions) { + definitionsPool = { ...definitions, ...this.definitions } + } else { + this.definitions = definitions + definitionsPool = definitions + } + const fullUsage = [ `${description}`, '', @@ -35,14 +44,14 @@ class BaseCommand { if (seenExclusive.has(param)) { continue } - const { exclusive } = definitions[param] - let paramUsage = `${definitions[param].usage}` + const exclusive = definitionsPool[param]?.exclusive + let paramUsage = definitionsPool[param]?.usage if (exclusive) { const exclusiveParams = [paramUsage] seenExclusive.add(param) for (const e of exclusive) { seenExclusive.add(e) - exclusiveParams.push(definitions[e].usage) + exclusiveParams.push(definitionsPool[e].usage) } paramUsage = `${exclusiveParams.join('|')}` } @@ -77,7 +86,7 @@ class BaseCommand { constructor (npm) { this.npm = npm - const { config } = this.npm + const { config } = this if (!this.constructor.skipConfigValidation) { config.validate() @@ -88,6 +97,11 @@ class BaseCommand { } } + get config () { + // Return command-specific config if it exists, otherwise use npm's config + return this.npm.config + } + get name () { return this.constructor.name } diff --git a/lib/npm.js b/lib/npm.js index c635f3e05a7b3..82abaec752361 100644 --- a/lib/npm.js +++ b/lib/npm.js @@ -37,6 +37,8 @@ class Npm { #runId = new Date().toISOString().replace(/[.:]/g, '_') #title = 'npm' #argvClean = [] + #argv = undefined + #excludeNpmCwd = undefined #npmRoot = null #display = null @@ -227,6 +229,14 @@ class Npm { process.env.npm_command = this.command } + if (!Command.definitions || Command.definitions === definitions) { + this.config.logWarnings() + } else { + this.config.loadCommand(Command.definitions) + this.config.logWarnings() + this.config.warn = true + } + if (this.config.get('usage')) { return output.standard(command.usage) } diff --git a/tap-snapshots/test/lib/commands/install.js.test.cjs b/tap-snapshots/test/lib/commands/install.js.test.cjs index 3c9fa9bbec447..d5315397aaf4e 100644 --- a/tap-snapshots/test/lib/commands/install.js.test.cjs +++ b/tap-snapshots/test/lib/commands/install.js.test.cjs @@ -134,9 +134,9 @@ silly logfile done cleaning log files verbose stack Error: The developer of this package has specified the following through devEngines verbose stack Invalid devEngines.runtime verbose stack Invalid name "nondescript" does not match "node" for "runtime" -verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:181:27) -verbose stack at MockNpm.#exec ({CWD}/lib/npm.js:252:7) -verbose stack at MockNpm.exec ({CWD}/lib/npm.js:208:9) +verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:195:27) +verbose stack at MockNpm.#exec ({CWD}/lib/npm.js:262:7) +verbose stack at MockNpm.exec ({CWD}/lib/npm.js:210:9) error code EBADDEVENGINES error EBADDEVENGINES The developer of this package has specified the following through devEngines error EBADDEVENGINES Invalid devEngines.runtime @@ -199,9 +199,9 @@ warn EBADDEVENGINES } verbose stack Error: The developer of this package has specified the following through devEngines verbose stack Invalid devEngines.runtime verbose stack Invalid name "nondescript" does not match "node" for "runtime" -verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:181:27) -verbose stack at MockNpm.#exec ({CWD}/lib/npm.js:252:7) -verbose stack at MockNpm.exec ({CWD}/lib/npm.js:208:9) +verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:195:27) +verbose stack at MockNpm.#exec ({CWD}/lib/npm.js:262:7) +verbose stack at MockNpm.exec ({CWD}/lib/npm.js:210:9) error code EBADDEVENGINES error EBADDEVENGINES The developer of this package has specified the following through devEngines error EBADDEVENGINES Invalid devEngines.runtime @@ -225,9 +225,9 @@ silly logfile done cleaning log files verbose stack Error: The developer of this package has specified the following through devEngines verbose stack Invalid devEngines.runtime verbose stack Invalid name "nondescript" does not match "node" for "runtime" -verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:181:27) -verbose stack at MockNpm.#exec ({CWD}/lib/npm.js:252:7) -verbose stack at MockNpm.exec ({CWD}/lib/npm.js:208:9) +verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:195:27) +verbose stack at MockNpm.#exec ({CWD}/lib/npm.js:262:7) +verbose stack at MockNpm.exec ({CWD}/lib/npm.js:210:9) error code EBADDEVENGINES error EBADDEVENGINES The developer of this package has specified the following through devEngines error EBADDEVENGINES Invalid devEngines.runtime diff --git a/test/lib/npm.js b/test/lib/npm.js index b4ac509adb495..6fe566070cf5d 100644 --- a/test/lib/npm.js +++ b/test/lib/npm.js @@ -567,3 +567,132 @@ t.test('print usage if non-command param provided', async t => { t.match(joinedOutput(), 'Unknown command: "tset"') t.match(joinedOutput(), 'Did you mean this?') }) + +async function testCommandDefinitions (t, { defaultValue, outputValue, type, flags }) { + const path = require('node:path') + + // Create a temporary command file + const tsetPath = path.join(__dirname, '../../lib/commands/tset.js') + const tsetContent = ` +const Definition = require('@npmcli/config/lib/definitions/definition.js') +const BaseCommand = require('../base-cmd.js') +const { output } = require('proc-log') +const { flatten } = require('@npmcli/config/lib/definitions/index.js') + +module.exports = class TestCommand extends BaseCommand { + static description = 'A test command' + static name = 'tset' + static definitions = { + say: new Definition('say', { + default: ${defaultValue}, + type: ${type}, + description: 'say', + flatten, + }), + } + + async exec () { + const say = this.npm.config.get('say') + output.standard(say) + } +} +` + fs.writeFileSync(tsetPath, tsetContent) + t.teardown(() => { + try { + fs.unlinkSync(tsetPath) + delete require.cache[tsetPath] + } catch (e) { + // ignore + } + }) + + const mockCmdList = require('../../lib/utils/cmd-list.js') + const { npm, joinedOutput } = await loadMockNpm(t, { + argv: ['tset', ...(flags || [])], + mocks: { + '{LIB}/utils/cmd-list.js': { + ...mockCmdList, + commands: [...mockCmdList.commands, 'tset'], + deref: (c) => c === 'tset' ? 'tset' : mockCmdList.deref(c), + }, + }, + }) + + // Now you can execute the mocked command + await npm.exec('tset', []) + + t.match(joinedOutput(), outputValue) +} + +const stack = { + boolean_default: (t) => testCommandDefinitions(t, { type: 'Boolean', defaultValue: 'false', outputValue: 'false' }), + string_default: (t) => testCommandDefinitions(t, { type: 'String', defaultValue: `'meow'`, outputValue: 'meow' }), + string_flag: (t) => testCommandDefinitions(t, { type: 'String', defaultValue: `'meow'`, outputValue: 'woof', flags: ['--say=woof'] }), +} + +Object.entries(stack).forEach(([name, fn]) => { + t.test(name, fn) +}) + +t.test('help includes both global and command definitions', async t => { + const path = require('node:path') + + // Create a temporary command file + const tsetPath = path.join(__dirname, '../../lib/commands/tset.js') + const tsetContent = ` +const Definition = require('@npmcli/config/lib/definitions/definition.js') +const BaseCommand = require('../base-cmd.js') +const { output } = require('proc-log') +const { flatten } = require('@npmcli/config/lib/definitions/index.js') + +module.exports = class TestCommand extends BaseCommand { + static description = 'A test command' + static name = 'tset' + static params = ['yes', 'say'] + static definitions = { + say: new Definition('say', { + default: 'meow', + type: String, + description: 'what to say', + flatten, + }), + } + + async exec () { + const say = this.npm.config.get('say') + output.standard(say) + } +} +` + fs.writeFileSync(tsetPath, tsetContent) + t.teardown(() => { + try { + fs.unlinkSync(tsetPath) + delete require.cache[tsetPath] + } catch (e) { + // ignore + } + }) + + const mockCmdList = require('../../lib/utils/cmd-list.js') + const { npm, joinedOutput } = await loadMockNpm(t, { + argv: ['tset', '--help'], + mocks: { + '{LIB}/utils/cmd-list.js': { + ...mockCmdList, + commands: [...mockCmdList.commands, 'tset'], + deref: (c) => c === 'tset' ? 'tset' : mockCmdList.deref(c), + }, + }, + }) + + await npm.exec('tset', []) + + const output = joinedOutput() + // Check that both global definition (yes) and command definition (say) appear in help + t.match(output, /--yes/, 'help includes global definition --yes') + t.match(output, /-y\|--yes/, 'help includes short flag -y for yes') + t.match(output, /--say/, 'help includes command definition --say') + t.match(output, /--say /, 'help includes --say with hint') +}) diff --git a/workspaces/config/lib/index.js b/workspaces/config/lib/index.js index 0ad716ccb069f..443c76e202e6c 100644 --- a/workspaces/config/lib/index.js +++ b/workspaces/config/lib/index.js @@ -51,6 +51,7 @@ const confTypes = new Set([ 'builtin', ...confFileTypes, 'env', + 'flags', 'cli', ]) @@ -59,6 +60,7 @@ class Config { #flatten // populated the first time we flatten the object #flatOptions = null + #warnings = [] static get typeDefs () { return typeDefs @@ -82,17 +84,9 @@ class Config { this.nerfDarts = nerfDarts this.definitions = definitions // turn the definitions into nopt's weirdo syntax - const types = {} - const defaults = {} - this.deprecated = {} - for (const [key, def] of Object.entries(definitions)) { - defaults[key] = def.default - types[key] = def.type - if (def.deprecated) { - this.deprecated[key] = def.deprecated.trim().replace(/\n +/, '\n') - } - } + const { types, defaults, deprecated } = this.getTypesFromDefinitions(definitions) + this.deprecated = deprecated this.#flatten = flatten this.types = types this.shorthands = shorthands @@ -137,6 +131,135 @@ class Config { } this.#loaded = false + + this.warn = true + + this.log = { + warn: (type, ...args) => { + if (!this.warn) { + this.#warnings.push({ type, args }) + } else { + log.warn(...args) + } + }, + } + } + + #checkDeprecated (key) { + if (this.deprecated[key]) { + this.log.warn(`deprecated:${key}`, 'config', key, this.deprecated[key]) + } + } + + #getFlags (types) { + for (const s of Object.keys(this.shorthands)) { + if (s.length > 1 && this.argv.includes(`-${s}`)) { + log.warn(`-${s} is not a valid single-hyphen cli flag and will be removed in the future`) + } + } + nopt.invalidHandler = (k, val, type) => + this.invalidHandler(k, val, type, 'command line options', 'cli') + nopt.unknownHandler = this.unknownHandler + nopt.abbrevHandler = this.abbrevHandler + const conf = nopt(types, this.shorthands, this.argv) + nopt.invalidHandler = null + nopt.unknownHandler = null + this.parsedArgv = conf.argv + delete conf.argv + return conf + } + + #getOneOfKeywords (mustBe, typeDesc) { + let keyword + if (mustBe.length === 1 && typeDesc.includes(Array)) { + keyword = ' one or more' + } else if (mustBe.length > 1 && typeDesc.includes(Array)) { + keyword = ' one or more of:' + } else if (mustBe.length > 1) { + keyword = ' one of:' + } else { + keyword = '' + } + return keyword + } + + #loadObject (obj, where, source, er = null) { + // obj is the raw data read from the file + const conf = this.data.get(where) + if (conf.source) { + const m = `double-loading "${where}" configs from ${source}, ` + + `previously loaded from ${conf.source}` + throw new Error(m) + } + + if (this.sources.has(source)) { + const m = `double-loading config "${source}" as "${where}", ` + + `previously loaded as "${this.sources.get(source)}"` + throw new Error(m) + } + + conf.source = source + this.sources.set(source, where) + if (er) { + conf.loadError = er + if (er.code !== 'ENOENT') { + log.verbose('config', `error loading ${where} config`, er) + } + } else { + conf.raw = obj + for (const [key, value] of Object.entries(obj)) { + const k = envReplace(key, this.env) + const v = this.parseField(value, k) + if (where !== 'default') { + this.#checkDeprecated(k) + if (this.definitions[key]?.exclusive) { + for (const exclusive of this.definitions[key].exclusive) { + if (!this.isDefault(exclusive)) { + throw new TypeError(`--${key} cannot be provided when using --${exclusive}`) + } + } + } + } + if (where !== 'default' || key === 'npm-version') { + this.checkUnknown(where, key) + } + conf.data[k] = v + } + } + } + + async #loadFile (file, type) { + // only catch the error from readFile, not from the loadObject call + log.silly('config', `load:file:${file}`) + await readFile(file, 'utf8').then( + data => { + const parsedConfig = ini.parse(data) + if (type === 'project' && parsedConfig.prefix) { + // Log error if prefix is mentioned in project .npmrc + /* eslint-disable-next-line max-len */ + log.error('config', `prefix cannot be changed from project config: ${file}.`) + } + return this.#loadObject(parsedConfig, type, file) + }, + er => this.#loadObject(null, type, file, er) + ) + } + + getTypesFromDefinitions (definitions) { + if (!definitions) { + definitions = {} + } + const types = {} + const defaults = {} + const deprecated = {} + for (const [key, def] of Object.entries(definitions)) { + defaults[key] = def.default + types[key] = def.type + if (def.deprecated) { + deprecated[key] = def.deprecated.trim().replace(/\n +/, '\n') + } + } + return { types, defaults, deprecated } } get list () { @@ -155,6 +278,29 @@ class Config { return this.#get('global') ? this.globalPrefix : this.localPrefix } + removeWarnings (types) { + const typeSet = new Set(Array.isArray(types) ? types : [types]) + this.#warnings = this.#warnings.filter(w => !typeSet.has(w.type)) + } + + #deduplicateWarnings () { + const seen = new Set() + this.#warnings = this.#warnings.filter(w => { + if (seen.has(w.type)) { + return false + } + seen.add(w.type) + return true + }) + } + + logWarnings () { + for (const warning of this.#warnings) { + log.warn(...warning.args) + } + this.#warnings = [] + } + // return the location where key is found. find (key) { if (!this.loaded) { @@ -172,13 +318,6 @@ class Config { return null } - get (key, where) { - if (!this.loaded) { - throw new Error('call config.load() before reading values') - } - return this.#get(key, where) - } - // we need to get values sometimes, so use this internal one to do so // while in the process of loading. #get (key, where = null) { @@ -189,6 +328,13 @@ class Config { return where === null || hasOwnProperty(data, key) ? data[key] : undefined } + get (key, where) { + if (!this.loaded) { + throw new Error('call config.load() before reading values') + } + return this.#get(key, where) + } + set (key, val, where = 'cli') { if (!this.loaded) { throw new Error('call config.load() before setting values') @@ -362,23 +508,34 @@ class Config { } loadCLI () { - for (const s of Object.keys(this.shorthands)) { - if (s.length > 1 && this.argv.includes(`-${s}`)) { - log.warn(`-${s} is not a valid single-hyphen cli flag and will be removed in the future`) - } - } - nopt.invalidHandler = (k, val, type) => - this.invalidHandler(k, val, type, 'command line options', 'cli') - nopt.unknownHandler = this.unknownHandler - nopt.abbrevHandler = this.abbrevHandler - const conf = nopt(this.types, this.shorthands, this.argv) - nopt.invalidHandler = null - nopt.unknownHandler = null - this.parsedArgv = conf.argv - delete conf.argv + const conf = this.#getFlags(this.types) this.#loadObject(conf, 'cli', 'command line options') } + loadCommand (definitions) { + // Merge command definitions with global definitions + this.definitions = { ...this.definitions, ...definitions } + const { defaults, types, deprecated } = this.getTypesFromDefinitions(definitions) + this.deprecated = { ...this.deprecated, ...deprecated } + this.types = { ...this.types, ...types } + + // Re-parse with merged definitions + const conf = this.#getFlags(this.types) + + // Remove warnings for keys that are now defined + const keysToRemove = Object.keys(definitions).flatMap(key => [ + `unknown:${key}`, + `deprecated:${key}`, + ]) + this.removeWarnings(keysToRemove) + + // Load into new command source - only command-specific defaults + parsed flags + this.#loadObject({ ...defaults, ...conf }, 'flags', 'command-specific flag options') + + // Deduplicate warnings by type (e.g., unknown:key warnings from both cli and flags) + this.#deduplicateWarnings() + } + get valid () { for (const [where, { valid }] of this.data.entries()) { if (valid === false || valid === null && !this.validate(where)) { @@ -510,7 +667,8 @@ class Config { invalidHandler (k, val, type, source, where) { const typeDescription = require('./type-description.js') - log.warn( + this.log.warn( + 'invalid', 'invalid config', k + '=' + JSON.stringify(val), `set in ${source}` @@ -536,7 +694,7 @@ class Config { const msg = 'Must be' + this.#getOneOfKeywords(mustBe, typeDesc) const desc = mustBe.length === 1 ? mustBe[0] : [...new Set(mustBe.map(n => typeof n === 'string' ? n : JSON.stringify(n)))].join(', ') - log.warn('invalid config', msg, desc) + this.log.warn('invalid', 'invalid config', msg, desc) } abbrevHandler (short, long) { @@ -549,109 +707,27 @@ class Config { } } - #getOneOfKeywords (mustBe, typeDesc) { - let keyword - if (mustBe.length === 1 && typeDesc.includes(Array)) { - keyword = ' one or more' - } else if (mustBe.length > 1 && typeDesc.includes(Array)) { - keyword = ' one or more of:' - } else if (mustBe.length > 1) { - keyword = ' one of:' - } else { - keyword = '' - } - return keyword - } - - #loadObject (obj, where, source, er = null) { - // obj is the raw data read from the file - const conf = this.data.get(where) - if (conf.source) { - const m = `double-loading "${where}" configs from ${source}, ` + - `previously loaded from ${conf.source}` - throw new Error(m) - } - - if (this.sources.has(source)) { - const m = `double-loading config "${source}" as "${where}", ` + - `previously loaded as "${this.sources.get(source)}"` - throw new Error(m) - } - - conf.source = source - this.sources.set(source, where) - if (er) { - conf.loadError = er - if (er.code !== 'ENOENT') { - log.verbose('config', `error loading ${where} config`, er) - } - } else { - conf.raw = obj - for (const [key, value] of Object.entries(obj)) { - const k = envReplace(key, this.env) - const v = this.parseField(value, k) - if (where !== 'default') { - this.#checkDeprecated(k) - if (this.definitions[key]?.exclusive) { - for (const exclusive of this.definitions[key].exclusive) { - if (!this.isDefault(exclusive)) { - throw new TypeError(`--${key} cannot be provided when using --${exclusive}`) - } - } - } - } - if (where !== 'default' || key === 'npm-version') { - this.checkUnknown(where, key) - } - conf.data[k] = v - } - } - } - checkUnknown (where, key) { if (!this.definitions[key]) { if (internalEnv.includes(key)) { return } if (!key.includes(':')) { - log.warn(`Unknown ${where} config "${where === 'cli' ? '--' : ''}${key}". This will stop working in the next major version of npm.`) + this.log.warn(`unknown:${key}`, `Unknown ${where} config "${where === 'cli' || where === 'flags' ? '--' : ''}${key}". This will stop working in the next major version of npm.`) return } const baseKey = key.split(':').pop() if (!this.definitions[baseKey] && !this.nerfDarts.includes(baseKey)) { - log.warn(`Unknown ${where} config "${baseKey}" (${key}). This will stop working in the next major version of npm.`) + this.log.warn(`unknown:${baseKey}`, `Unknown ${where} config "${baseKey}" (${key}). This will stop working in the next major version of npm.`) } } } - #checkDeprecated (key) { - if (this.deprecated[key]) { - log.warn('config', key, this.deprecated[key]) - } - } - // Parse a field, coercing it to the best type available. parseField (f, key, listElement = false) { return parseField(f, key, this, listElement) } - async #loadFile (file, type) { - // only catch the error from readFile, not from the loadObject call - log.silly('config', `load:file:${file}`) - await readFile(file, 'utf8').then( - data => { - const parsedConfig = ini.parse(data) - if (type === 'project' && parsedConfig.prefix) { - // Log error if prefix is mentioned in project .npmrc - /* eslint-disable-next-line max-len */ - log.error('config', `prefix cannot be changed from project config: ${file}.`) - } - return this.#loadObject(parsedConfig, type, file) - }, - er => this.#loadObject(null, type, file, er) - ) - } - loadBuiltinConfig () { return this.#loadFile(resolve(this.npmPath, 'npmrc'), 'builtin') } diff --git a/workspaces/config/test/index.js b/workspaces/config/test/index.js index f60070d419bfd..c927ae52ba219 100644 --- a/workspaces/config/test/index.js +++ b/workspaces/config/test/index.js @@ -1144,7 +1144,7 @@ t.test('nerfdart auths set at the top level into the registry', async t => { // now we go ahead and do the repair, and save c.repair() await c.save('user') - t.same(c.list[3], expect) + t.same(c.data.get('user').data, expect) }) } }) @@ -1587,3 +1587,273 @@ t.test('abbreviation expansion warnings', async t => { ['warn', 'Expanding --bef to --before. This will stop working in the next major version of npm'], ], 'Warns about expanded abbreviations') }) + +t.test('warning suppression and logging', async t => { + const path = t.testdir() + const logs = [] + const logHandler = (...args) => logs.push(args) + process.on('log', logHandler) + t.teardown(() => process.off('log', logHandler)) + + const config = new Config({ + npmPath: `${path}/npm`, + env: {}, + argv: [process.execPath, __filename, '--unknown-key', 'value'], + cwd: path, + shorthands, + definitions, + nerfDarts, + }) + + // Load first to collect warnings + await config.load() + + // Now disable warnings and trigger more + config.warn = false + config.log.warn('test-type', 'test warning 1') + config.log.warn('test-type2', 'test warning 2') + + // Should have warnings collected but not logged + const initialWarnings = logs.filter(l => l[0] === 'warn') + const beforeCount = initialWarnings.length + + // Now log the warnings + config.warn = true + config.logWarnings() + const afterLogging = logs.filter(l => l[0] === 'warn') + t.ok(afterLogging.length > beforeCount, 'warnings logged after logWarnings()') + + // Calling logWarnings again should not add more warnings + const warningCount = afterLogging.length + config.logWarnings() + const finalWarnings = logs.filter(l => l[0] === 'warn') + t.equal(finalWarnings.length, warningCount, 'no duplicate warnings after second logWarnings()') +}) + +t.test('removeWarnings', async t => { + const path = t.testdir() + const logs = [] + const logHandler = (...args) => logs.push(args) + process.on('log', logHandler) + t.teardown(() => process.off('log', logHandler)) + + const config = new Config({ + npmPath: `${path}/npm`, + env: {}, + argv: [process.execPath, __filename, '--unknown1', 'value', '--unknown2', 'value'], + cwd: path, + shorthands, + definitions, + nerfDarts, + }) + + config.warn = false + await config.load() + + // Remove specific warning types + config.removeWarnings('unknown:unknown1') + config.logWarnings() + + const warnings = logs.filter(l => l[0] === 'warn') + const hasUnknown1 = warnings.some(w => w[1].includes('unknown1')) + const hasUnknown2 = warnings.some(w => w[1].includes('unknown2')) + + t.notOk(hasUnknown1, 'unknown1 warning removed') + t.ok(hasUnknown2, 'unknown2 warning still present') +}) + +t.test('removeWarnings with array', async t => { + const path = t.testdir() + const logs = [] + const logHandler = (...args) => logs.push(args) + process.on('log', logHandler) + t.teardown(() => process.off('log', logHandler)) + + const config = new Config({ + npmPath: `${path}/npm`, + env: {}, + argv: [process.execPath, __filename, '--unknown1', 'value', '--unknown2', 'value'], + cwd: path, + shorthands, + definitions, + nerfDarts, + }) + + config.warn = false + await config.load() + + // Count warnings before removal + const beforeRemoval = logs.filter(l => l[0] === 'warn').length + + // Remove multiple warning types + config.removeWarnings(['unknown:unknown1', 'unknown:unknown2']) + config.logWarnings() + + const warnings = logs.filter(l => l[0] === 'warn') + // Check that no new unknown1 or unknown2 warnings were added + const hasUnknown1 = warnings.slice(beforeRemoval).some(w => w[1].includes('unknown1')) + const hasUnknown2 = warnings.slice(beforeRemoval).some(w => w[1].includes('unknown2')) + t.notOk(hasUnknown1, 'unknown1 warnings removed') + t.notOk(hasUnknown2, 'unknown2 warnings removed') +}) + +t.test('loadCommand method', async t => { + const path = t.testdir() + const logs = [] + const logHandler = (...args) => logs.push(args) + process.on('log', logHandler) + t.teardown(() => process.off('log', logHandler)) + + const commandDefs = createDef('cmd-option', { + default: false, + type: Boolean, + description: 'A command-specific option', + }) + + const config = new Config({ + npmPath: `${path}/npm`, + env: {}, + argv: [process.execPath, __filename, '--cmd-option', '--unknown-cmd'], + cwd: path, + shorthands, + definitions, + nerfDarts, + }) + + config.warn = false + await config.load() + + // Load command-specific definitions + config.loadCommand(commandDefs) + + // Check that cmd-option is now recognized and set to true + t.equal(config.get('cmd-option'), true, 'command option loaded from CLI') + + // Check that warnings were removed for the now-defined key + config.logWarnings() + const warnings = logs.filter(l => l[0] === 'warn' && l[1].includes('cmd-option')) + t.equal(warnings.length, 0, 'no warnings for now-defined cmd-option') + + // Check that unknown-cmd still generates a warning + const unknownWarnings = logs.filter(l => l[0] === 'warn' && l[1].includes('unknown-cmd')) + t.ok(unknownWarnings.length > 0, 'unknown-cmd still generates warning') +}) + +t.test('loadCommand with deprecated definitions', async t => { + const path = t.testdir() + const logs = [] + const logHandler = (...args) => logs.push(args) + process.on('log', logHandler) + t.teardown(() => process.off('log', logHandler)) + + const commandDefs = createDef('deprecated-opt', { + default: 'default', + type: String, + description: 'A deprecated option', + deprecated: 'This option is deprecated', + }) + + const config = new Config({ + npmPath: `${path}/npm`, + env: {}, + argv: [process.execPath, __filename, '--deprecated-opt', 'value'], + cwd: path, + shorthands, + definitions, + nerfDarts, + }) + + await config.load() + config.loadCommand(commandDefs) + + // Should have deprecation warning + const deprecatedWarnings = logs.filter(l => + l[0] === 'warn' && l[1] === 'config' && l[2] === 'deprecated-opt' + ) + t.ok(deprecatedWarnings.length > 0, 'deprecated option warning logged') +}) + +t.test('getTypesFromDefinitions with no definitions', async t => { + const config = new Config({ + npmPath: t.testdir(), + env: {}, + argv: [process.execPath, __filename], + cwd: process.cwd(), + shorthands, + definitions, + nerfDarts, + }) + + const result = config.getTypesFromDefinitions(undefined) + t.ok(result.types, 'returns types object') + t.ok(result.defaults, 'returns defaults object') + t.ok(result.deprecated, 'returns deprecated object') + t.same(Object.keys(result.types), [], 'empty types for undefined definitions') +}) + +t.test('prefix getter when global is true', async t => { + const path = t.testdir() + const config = new Config({ + npmPath: `${path}/npm`, + env: {}, + argv: [process.execPath, __filename, '--global'], + cwd: path, + shorthands, + definitions, + nerfDarts, + }) + + await config.load() + t.equal(config.prefix, config.globalPrefix, 'prefix returns globalPrefix when global=true') +}) + +t.test('prefix getter when global is false', async t => { + const path = t.testdir() + const config = new Config({ + npmPath: `${path}/npm`, + env: {}, + argv: [process.execPath, __filename], + cwd: path, + shorthands, + definitions, + nerfDarts, + }) + + await config.load() + t.equal(config.prefix, config.localPrefix, 'prefix returns localPrefix when global=false') +}) + +t.test('find throws when config not loaded', async t => { + const config = new Config({ + npmPath: t.testdir(), + env: {}, + argv: [process.execPath, __filename], + cwd: process.cwd(), + shorthands, + definitions, + nerfDarts, + }) + + t.throws( + () => config.find('registry'), + /call config\.load\(\) before reading values/, + 'find throws before load' + ) +}) + +t.test('valid getter with invalid config', async t => { + const path = t.testdir() + const config = new Config({ + npmPath: `${path}/npm`, + env: {}, + argv: [process.execPath, __filename, '--maxsockets', 'not-a-number'], + cwd: path, + shorthands, + definitions, + nerfDarts, + }) + + await config.load() + const isValid = config.valid + t.notOk(isValid, 'config is invalid when it has invalid values') +})