From ab62a124fe4408c75a245fbd454456e39d474eb6 Mon Sep 17 00:00:00 2001 From: knownshah Date: Sun, 27 Sep 2026 20:01:08 +0500 Subject: [PATCH] feat(checks): hint engine-strict when engines.node is set When package.json defines engines.node and .npmrc does not enable engine-strict=true, surface an informational doctor hint so installs fail fast on mismatched Node versions. Closes #2 --- src/checks/node-version.ts | 36 ++++++++++++++++++++++++++++--- tests/checks/node-version.test.ts | 30 ++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/checks/node-version.ts b/src/checks/node-version.ts index 084973a..5b89923 100644 --- a/src/checks/node-version.ts +++ b/src/checks/node-version.ts @@ -3,6 +3,23 @@ import path from 'node:path'; import semver from 'semver'; import type { CheckDefinition, CheckResult, CheckContext } from '../types/index.js'; +const ENGINE_STRICT_HINT = + "Add 'engine-strict=true' to .npmrc to prevent accidental installs on mismatched Node versions."; + +/** Returns true if .npmrc enables engine-strict=true (non-comment line). */ +async function hasEngineStrict(cwd: string): Promise { + try { + const npmrc = await fs.readFile(path.join(cwd, '.npmrc'), 'utf8'); + return npmrc.split(/\r?\n/).some((line) => { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) return false; + return /^engine-strict\s*=\s*true\b/i.test(trimmed); + }); + } catch { + return false; + } +} + export const nodeVersionCheck: CheckDefinition = { id: 'node-version', name: 'Node.js Version', @@ -15,10 +32,11 @@ export const nodeVersionCheck: CheckDefinition = { let targetRange = context.config?.minNodeVersion || '>=18.0.0'; let source = 'default'; + const hasEnginesNode = Boolean(context.pkg?.engines?.node); // 1. Check package.json engines.node - if (context.pkg?.engines?.node) { - targetRange = context.pkg.engines.node; + if (hasEnginesNode) { + targetRange = context.pkg!.engines!.node as string; source = 'package.json engines'; } else { // 2. Check .nvmrc @@ -52,6 +70,15 @@ export const nodeVersionCheck: CheckDefinition = { const satisfies = semver.satisfies(cleanCurrent, targetRange, { loose: true }); + // When engines.node is set, suggest engine-strict if not already enabled + let engineStrictHint: string | undefined; + if (hasEnginesNode) { + const strictEnabled = await hasEngineStrict(context.cwd); + if (!strictEnabled) { + engineStrictHint = ENGINE_STRICT_HINT; + } + } + if (satisfies) { return { id: 'node-version', @@ -60,6 +87,7 @@ export const nodeVersionCheck: CheckDefinition = { status: 'success', message: `Node.js ${currentVersion} (satisfies ${targetRange} from ${source})`, fixable: false, + ...(engineStrictHint ? { hint: engineStrictHint } : {}), }; } @@ -75,7 +103,9 @@ export const nodeVersionCheck: CheckDefinition = { `Consider switching version with 'nvm use' or 'fnm use'`, ], fixable: false, - hint: `Install the supported Node.js version using your version manager (nvm, fnm, or volta).`, + hint: + engineStrictHint ?? + `Install the supported Node.js version using your version manager (nvm, fnm, or volta).`, }; }, }; diff --git a/tests/checks/node-version.test.ts b/tests/checks/node-version.test.ts index 552ddf3..8771bc0 100644 --- a/tests/checks/node-version.test.ts +++ b/tests/checks/node-version.test.ts @@ -44,4 +44,34 @@ describe('checks/node-version', () => { expect(result.message).toContain('.nvmrc'); }); + + it('hints engine-strict when engines.node is set and .npmrc lacks engine-strict', async () => { + const result = await nodeVersionCheck.run({ + cwd: testDir.path, + pkg: { engines: { node: '>=18.0.0' } }, + }); + + expect(result.hint).toContain('engine-strict=true'); + }); + + it('does not hint engine-strict when .npmrc already enables it', async () => { + await testDir.writeFile('.npmrc', 'engine-strict=true\n'); + + const result = await nodeVersionCheck.run({ + cwd: testDir.path, + pkg: { engines: { node: '>=18.0.0' } }, + }); + + expect(result.status).toBe('success'); + expect(result.hint).toBeUndefined(); + }); + + it('does not hint engine-strict when engines.node is absent', async () => { + const result = await nodeVersionCheck.run({ + cwd: testDir.path, + pkg: {}, + }); + + expect(result.hint).toBeUndefined(); + }); });