From ad7e124167442be5c00e0f35c299016c0081c563 Mon Sep 17 00:00:00 2001 From: Jeff Wainwright Date: Sun, 21 Apr 2024 20:59:59 -0700 Subject: [PATCH] chore: adds better validation --- package.json | 4 +- pnpm-lock.yaml | 26 ++++- src/scripts/core.ts | 12 +- src/types.ts | 1 + test/core.test.ts | 19 ++-- test/scripts.test.ts | 262 ------------------------------------------- 6 files changed, 48 insertions(+), 276 deletions(-) delete mode 100644 test/scripts.test.ts diff --git a/package.json b/package.json index 7dd3808..f6b6a41 100644 --- a/package.json +++ b/package.json @@ -54,13 +54,15 @@ "author": "Jeff Wainwright (https://jeffry.in)", "license": "MIT", "dependencies": { + "@types/validate-npm-package-name": "^4.0.2", "commander": "^11.1.0", "cosmiconfig": "^9.0.0", "execa": "^8.0.1", "fast-glob": "^3.3.2", "gradient-string": "^2.0.2", "ora": "8.0.1", - "rimraf": "^5.0.5" + "rimraf": "^5.0.5", + "validate-npm-package-name": "^5.0.0" }, "devDependencies": { "@commitlint/cli": "^18.6.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e68a686..44f1216 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@types/validate-npm-package-name': + specifier: ^4.0.2 + version: 4.0.2 commander: specifier: ^11.1.0 version: 11.1.0 @@ -29,6 +32,9 @@ importers: rimraf: specifier: ^5.0.5 version: 5.0.5 + validate-npm-package-name: + specifier: ^5.0.0 + version: 5.0.0 devDependencies: '@commitlint/cli': specifier: ^18.6.1 @@ -2008,6 +2014,10 @@ packages: resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} dev: true + /@types/validate-npm-package-name@4.0.2: + resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} + dev: false + /@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.5): resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} engines: {node: ^16.0.0 || >=18.0.0} @@ -2520,6 +2530,12 @@ packages: ieee754: 1.2.1 dev: true + /builtins@5.1.0: + resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==} + dependencies: + semver: 7.6.0 + dev: false + /bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -5749,7 +5765,6 @@ packages: engines: {node: '>=10'} dependencies: yallist: 4.0.0 - dev: true /lru-cache@7.18.3: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} @@ -7950,7 +7965,6 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 - dev: true /set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} @@ -8972,6 +8986,13 @@ packages: spdx-expression-parse: 3.0.1 dev: true + /validate-npm-package-name@5.0.0: + resolution: {integrity: sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dependencies: + builtins: 5.1.0 + dev: false + /vfile-message@3.1.4: resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==} requiresBuild: true @@ -9316,7 +9337,6 @@ packages: /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true /yaml@2.3.4: resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} diff --git a/src/scripts/core.ts b/src/scripts/core.ts index 7123b7c..860b829 100755 --- a/src/scripts/core.ts +++ b/src/scripts/core.ts @@ -1,4 +1,5 @@ import { readFileSync, writeFileSync } from 'fs' +import validatePackageName from 'validate-npm-package-name' import { execa } from 'execa' import fg from 'fast-glob' const { sync: glob } = fg @@ -27,6 +28,7 @@ export const constructVersionMap = async ({ debug = false, yarnConfig = false, isTesting = false, + validate = validatePackageName, }: ConstructVersionMapOptions) => { const updatedCodeDependencies = await Promise.all( codependencies.map(async (item) => { @@ -35,8 +37,10 @@ export const constructVersionMap = async ({ return item } else if (typeof item === 'string' && item.length > 1 && !item.includes(' ')) { // the following 2 lines capture only accepted npm package names - const isModuleSafeCharacters = /^[A-Za-z0-9\-_.]+$/.test(item) - if (!isModuleSafeCharacters) throw 'invalid item' + const { validForNewPackages, validForOldPackages, errors } = validate(item) + const isValid = [validForNewPackages, validForOldPackages].every((valid) => valid === true) + if (!isValid) throw new Error(errors?.join(', ')) + const runner = !yarnConfig ? 'npm' : 'yarn' const cmd = !yarnConfig ? ['view', item, 'version', 'latest'] @@ -44,10 +48,12 @@ export const constructVersionMap = async ({ const { stdout = '' } = (await exec(runner, cmd)) as unknown as Record const version = !yarnConfig ? stdout.replace('\n', '') : JSON.parse(stdout.replace('\n', ''))?.version + if (version) return { [item]: version } + throw `${version}` } else { - throw 'invalid item' + throw 'invalid item type' } } catch (err) { if (debug) diff --git a/src/types.ts b/src/types.ts index a05ae12..b77a346 100644 --- a/src/types.ts +++ b/src/types.ts @@ -95,4 +95,5 @@ export type ConstructVersionMapOptions = { debug?: boolean yarnConfig?: boolean isTesting?: boolean + validate?: any } diff --git a/test/core.test.ts b/test/core.test.ts index 8cb74f0..9eea18b 100644 --- a/test/core.test.ts +++ b/test/core.test.ts @@ -11,21 +11,20 @@ const { checkFiles, } = scripts -vi.mock('../src/script', () => { - const scripts = { - execPromise: vi.fn(), - } - return scripts -}) - test('constructVersionMap => pass', async () => { const exec = vi.fn(() => ({ stdout: '4.0.0', stderr: '', })) as any + const validate = vi.fn(() => ({ + validForNewPackages: true, + validForOldPackages: true, + errors: [], + })) const result = await constructVersionMap({ codependencies: ['lodash'], exec, + validate, }) expect(result).toEqual({ lodash: '4.0.0' }) }) @@ -35,10 +34,16 @@ test('constructVersionMap => fail', async () => { stdout: '', stderr: '', })) as any + const validate = vi.fn(() => ({ + validForNewPackages: false, + validForOldPackages: true, + errors: ['foo-bop', 'foo-beep'], + })) const result = await constructVersionMap({ codependencies: ['lodash'], exec, isTesting: true, + validate, }) expect(result).toEqual({}) }) diff --git a/test/scripts.test.ts b/test/scripts.test.ts deleted file mode 100644 index 8cb74f0..0000000 --- a/test/scripts.test.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { expect, test, vi } from 'vitest' -import * as scripts from '../src/scripts/core' -const { - constructVersionMap, - constructVersionTypes, - constructDepsToUpdateList, - constructDeps, - constructJson, - checkDependenciesForVersion, - checkMatches, - checkFiles, -} = scripts - -vi.mock('../src/script', () => { - const scripts = { - execPromise: vi.fn(), - } - return scripts -}) - -test('constructVersionMap => pass', async () => { - const exec = vi.fn(() => ({ - stdout: '4.0.0', - stderr: '', - })) as any - const result = await constructVersionMap({ - codependencies: ['lodash'], - exec, - }) - expect(result).toEqual({ lodash: '4.0.0' }) -}) - -test('constructVersionMap => fail', async () => { - const exec = vi.fn(() => ({ - stdout: '', - stderr: '', - })) as any - const result = await constructVersionMap({ - codependencies: ['lodash'], - exec, - isTesting: true, - }) - expect(result).toEqual({}) -}) - -test('constructVersionTypes => with ^', () => { - const result = constructVersionTypes('^1.2.3') - expect(result).toEqual({ bumpCharacter: '^', bumpVersion: '^1.2.3', exactVersion: '1.2.3' }) -}) - -test('constructVersionTypes with no specifier', () => { - const { bumpVersion, exactVersion } = constructVersionTypes('1.2.3') - expect(bumpVersion).toEqual(exactVersion) -}) - -test('constructDepsToUpdateList => returns dep to update list with exact characters', () => { - const result = constructDepsToUpdateList({ foo: '1.0.0' }, { foo: '2.0.0' }) - expect(result).toEqual([ - { - name: 'foo', - exact: '2.0.0', - expected: '2.0.0', - actual: '1.0.0', - }, - ]) -}) - -test('constructDeps => with update', () => { - const json = { - name: 'foo', - version: '1.0.0', - dependencies: { bar: '1.0.0' }, - path: './test', - } - const depName = 'bar' - const depList = [{ name: 'bar', expected: '2.0.0', actual: '1.0.0', exact: '2.0.0' }] - const result = constructDeps(json, depName, depList) - expect(result).toEqual({ bar: '2.0.0' }) -}) - -test('constructDeps => with no deplist', () => { - const json = { - name: 'foo', - version: '1.0.0', - dependencies: { bar: '1.0.0' }, - path: './test', - } - const depName = 'bar' - const depList = [] - const result = constructDeps(json, depName, depList) - expect(result).not.toBeDefined() -}) - -test('constructDeps => with more deps', () => { - const json = { - name: 'foo', - version: '1.0.0', - dependencies: { bar: '1.0.0', biz: '1.0.0' }, - path: './test', - } - const depName = 'bar' - const depList = [ - { name: 'bar', expected: '2.0.0', actual: '1.0.0', exact: '2.0.0' }, - { name: 'biz', expected: '2.0.0', actual: '1.0.0', exact: '2.0.0' }, - ] - const result = constructDeps(json, depName, depList) - expect(result).toEqual({ bar: '2.0.0', biz: '2.0.0' }) -}) - -test('constructJson => with updates', () => { - const json = { - name: 'foo', - version: '1.0.0', - dependencies: { bar: '1.0.0', biz: '1.0.0' }, - path: './test', - } - const depsToUpdate = { - depList: [ - { name: 'bar', expected: '2.0.0', actual: '1.0.0', exact: '2.0.0' }, - { name: 'biz', expected: '2.0.0', actual: '1.0.0', exact: '2.0.0' }, - ], - peerDepList: [], - devDepList: [], - } - const result = constructJson(json, depsToUpdate) - expect(result).toStrictEqual({ - name: 'foo', - path: './test', - version: '1.0.0', - dependencies: { - bar: '2.0.0', - biz: '2.0.0', - }, - }) -}) - -test('checkDependenciesForVersion => has updates', () => { - const versionMap = { - foo: '2.0.0', - bar: '2.0.0', - } - const json = { - name: 'biz', - version: '1.0.0', - dependencies: { bar: '1.0.0', foo: '1.0.0' }, - path: './test', - } - const result = checkDependenciesForVersion(versionMap, json, { - isTesting: true, - }) - expect(result).toEqual(true) -}) - -test('checkDependenciesForVersion => has updates + special characters', () => { - const versionMap = { - foo: '2.0.0', - bar: '2.0.0', - } - const json = { - name: 'biz', - version: '1.0.0', - dependencies: { bar: '1.0.0', foo: '1.0.0' }, - path: './test', - } - const result = checkDependenciesForVersion(versionMap, json, { - isTesting: true, - }) - expect(result).toEqual(true) -}) - -test('checkDependenciesForVersion => no updates', () => { - const versionMap = { - foo: '1.0.0', - bar: '1.0.0', - } - const json = { - name: 'biz', - version: '1.0.0', - dependencies: { bar: '1.0.0', foo: '1.0.0' }, - path: './test', - } - const result = checkDependenciesForVersion(versionMap, json, { - isTesting: true, - }) - expect(result).toEqual(false) -}) - -test('checkDependenciesForVersion => no updates', () => { - vi.clearAllMocks() - const versionMap = { - foo: '1.0.0', - bar: '1.0.0', - } - const json = { - name: 'biz', - version: '1.0.0', - dependencies: { bar: '1.0.0', foo: '1.0.0' }, - path: './test', - } - const result = checkDependenciesForVersion(versionMap, json, { - isTesting: true, - }) - expect(result).toEqual(false) -}) - -test('checkMatches => no updates', () => { - vi.clearAllMocks() - const logCheckMatchesNoUpdates = vi.spyOn(console, 'log') - const versionMap = { - foo: '1.0.0', - bar: '1.0.0', - } - const rootDir = './test/' - const isTesting = true - const files = ['test-pass-package.json'] - checkMatches({ versionMap, files, isTesting, rootDir }) - expect(logCheckMatchesNoUpdates).toBeCalled() -}) - -test('checkMatches => with error', () => { - vi.clearAllMocks() - const logCheckMatchesWithError = vi.spyOn(console, 'error') - const versionMap = { - lodash: '4.18.0', - 'fs-extra': '5.0.0', - } - const rootDir = './test/' - const isTesting = true - const files = ['test-fail-package.json'] - checkMatches({ versionMap, files, isTesting, rootDir }) - expect(logCheckMatchesWithError).toBeCalled() -}) - -test('checkFiles => with no updates', async () => { - vi.clearAllMocks() - const logCheckFilesNoUpdates = vi.spyOn(console, 'log') - const codependencies = ['lodash', 'fs-extra'] - const rootDir = './test/' - const files = ['test-pass-package.json'] - await checkFiles({ codependencies, rootDir, files }) - expect(logCheckFilesNoUpdates).toBeCalled() -}) - -test('checkFiles => with updates', async () => { - vi.clearAllMocks() - const logCheckFilesWithUpdates = vi.spyOn(console, 'error') - const codependencies = ['lodash', 'fs-extra'] - const rootDir = './test/' - const files = ['test-fail-package.json'] - await checkFiles({ codependencies, rootDir, files }) - expect(logCheckFilesWithUpdates).toBeCalled() -}) - -test('checkFiles => with no codeps', async () => { - vi.clearAllMocks() - const logCheckFilesWithNoCoDeps = vi.spyOn(console, 'error') - const codependencies = null - const rootDir = './test/' - const files = ['test-fail-package.json'] - await checkFiles({ codependencies, rootDir, files, debug: true } as any) - expect(logCheckFilesWithNoCoDeps).toBeCalled() -})