diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..282aa40 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,32 @@ +name: Test jobs +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] +jobs: + test: + runs-on: ubuntu-22.04 + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version-file: ./.node-version + - name: Enable corepack + run: corepack enable + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + - uses: actions/cache@v3 + name: Setup pnpm cache + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Install dependencies + run: pnpm i --frozen-lockfile + - name: Test + run: pnpm run test diff --git a/src/bin/args-options.ts b/src/bin/args-options.ts new file mode 100644 index 0000000..ba5c88f --- /dev/null +++ b/src/bin/args-options.ts @@ -0,0 +1,38 @@ +import type { ParseArgsConfig } from 'node:util'; + +export const argsOptions = { + output: { + type: 'string', + multiple: false, + short: 'o', + default: './licenses.json', + }, + pretty: { + type: 'boolean', + multiple: false, + short: 'p', + default: false, + }, + recursive: { + type: 'boolean', + multiple: false, + short: 'r', + default: false, + }, + dev: { + type: 'boolean', + multiple: false, + short: 'd', + default: false, + }, + 'no-prod': { + type: 'boolean', + multiple: false, + default: false, + }, + 'no-optional': { + type: 'boolean', + multiple: false, + default: false, + }, +} satisfies ParseArgsConfig['options']; \ No newline at end of file diff --git a/src/bin/pnpm-license-exporter.ts b/src/bin/pnpm-license-exporter.ts index 01e8d13..102cae7 100644 --- a/src/bin/pnpm-license-exporter.ts +++ b/src/bin/pnpm-license-exporter.ts @@ -3,43 +3,7 @@ import { execSync } from 'node:child_process'; import { parseArgs, ParseArgsConfig } from 'node:util'; import { writeLicenses } from '../pnpm/export-license'; - -export const argsOptions = { - output: { - type: 'string', - multiple: false, - short: 'o', - default: './licenses.json', - }, - pretty: { - type: 'boolean', - multiple: false, - short: 'p', - default: false, - }, - recursive: { - type: 'boolean', - multiple: false, - short: 'r', - default: false, - }, - dev: { - type: 'boolean', - multiple: false, - short: 'd', - default: false, - }, - 'no-prod': { - type: 'boolean', - multiple: false, - default: false, - }, - 'no-optional': { - type: 'boolean', - multiple: false, - default: false, - }, -} satisfies ParseArgsConfig['options']; +import { argsOptions } from './args-options'; const nodeLinkerSetting = ( JSON.parse(execSync('pnpm config list --json').toString('utf8')) as { diff --git a/src/pnpm/export-license/utils/build-exported-pnpm-package-info.test.ts b/src/pnpm/export-license/utils/build-exported-pnpm-package-info.test.ts new file mode 100644 index 0000000..9d3fdf6 --- /dev/null +++ b/src/pnpm/export-license/utils/build-exported-pnpm-package-info.test.ts @@ -0,0 +1,41 @@ +import { nextLicense } from '../../../license-txt'; +import { buildExportedPnpmPackageInfo } from './build-exported-pnpm-package-info'; + +test('should return package info without licenseTxt', () => { + expect(buildExportedPnpmPackageInfo({ + name: '@ampproject/remapping', + version: '2.2.0', + path: '/Users/luco/ghq/sucomado-frontend/node_modules/.pnpm/@ampproject+remapping@2.2.0/node_modules/@ampproject/remapping', + license: 'Apache-2.0', + author: 'Justin Ridgewell', + homepage: 'https://github.com/ampproject/remapping#readme', + description: 'Remap sequential sourcemaps through transformations to point at the original source code', + }, undefined)).toStrictEqual({ + name: '@ampproject/remapping', + version: '2.2.0', + license: 'Apache-2.0', + author: 'Justin Ridgewell', + homepage: 'https://github.com/ampproject/remapping#readme', + description: 'Remap sequential sourcemaps through transformations to point at the original source code', + }); +}); + +test('should return package info with licenseTxt', () => { + expect(buildExportedPnpmPackageInfo({ + name: '@ampproject/remapping', + version: '2.2.0', + path: '/Users/luco/ghq/sucomado-frontend/node_modules/.pnpm/@ampproject+remapping@2.2.0/node_modules/@ampproject/remapping', + license: 'Apache-2.0', + author: 'Justin Ridgewell', + homepage: 'https://github.com/ampproject/remapping#readme', + description: 'Remap sequential sourcemaps through transformations to point at the original source code', + }, nextLicense)).toStrictEqual({ + name: '@ampproject/remapping', + version: '2.2.0', + license: 'Apache-2.0', + author: 'Justin Ridgewell', + homepage: 'https://github.com/ampproject/remapping#readme', + description: 'Remap sequential sourcemaps through transformations to point at the original source code', + licenseTxt: nextLicense, + }); +}); \ No newline at end of file diff --git a/src/pnpm/export-license/utils/build-exported-pnpm-package-info.ts b/src/pnpm/export-license/utils/build-exported-pnpm-package-info.ts new file mode 100644 index 0000000..1d42cf8 --- /dev/null +++ b/src/pnpm/export-license/utils/build-exported-pnpm-package-info.ts @@ -0,0 +1,9 @@ +import type { PnpmPackageInfo, ExportedPnpmPackageInfo } from '../types/export-licenses.types'; + +export function buildExportedPnpmPackageInfo(pnpmPackage: PnpmPackageInfo, licenseTxt: undefined | string): ExportedPnpmPackageInfo { + // remove path property + const { path, ...PnpmPackageInfo } = pnpmPackage; + return licenseTxt === undefined + ? PnpmPackageInfo + : { ...PnpmPackageInfo, licenseTxt }; +} \ No newline at end of file diff --git a/src/pnpm/export-license/utils/find-license-dirent.test.ts b/src/pnpm/export-license/utils/find-license-dirent.test.ts new file mode 100644 index 0000000..d5f6b7a --- /dev/null +++ b/src/pnpm/export-license/utils/find-license-dirent.test.ts @@ -0,0 +1,35 @@ +import { Mock } from 'vitest'; +import { findLicenseDirent } from './find-license-dirent'; +import { readdir } from 'node:fs/promises'; + +vitest.mock('node:fs/promises', () => ({ + readdir: vitest.fn().mockResolvedValue([ + { + name: 'LICENSE.md', + parentPath: '/Users/mock/luco-inc/pnpm-license-exporter/node_modules/.pnpm/vitest@1.5.0_@types+node@20.12.7/node_modules/vitest/', + path: '/Users/mock/luco-inc/pnpm-license-exporter/node_modules/.pnpm/vitest@1.5.0_@types+node@20.12.7/node_modules/vitest/', + }, + { + name: 'README.md', + parentPath: '/Users/mock/luco-inc/pnpm-license-exporter/node_modules/.pnpm/vitest@1.5.0_@types+node@20.12.7/node_modules/vitest/', + path: '/Users/mock/luco-inc/pnpm-license-exporter/node_modules/.pnpm/vitest@1.5.0_@types+node@20.12.7/node_modules/vitest/', + } + ]) +})); + +test('should return license dirent', async () => { + expect(await findLicenseDirent('/Users/mock/luco-inc/pnpm-license-exporter/node_modules/.pnpm/vitest@1.5.0_@types+node@20.12.7/node_modules/vitest')).toStrictEqual({ + name: 'LICENSE.md', + parentPath: '/Users/mock/luco-inc/pnpm-license-exporter/node_modules/.pnpm/vitest@1.5.0_@types+node@20.12.7/node_modules/vitest/', + path: '/Users/mock/luco-inc/pnpm-license-exporter/node_modules/.pnpm/vitest@1.5.0_@types+node@20.12.7/node_modules/vitest/' + }); +}); + +test('should return undefined', async () => { + (readdir as Mock).mockResolvedValue([{ + name: 'README.md', + parentPath: '/Users/mock/luco-inc/pnpm-license-exporter/node_modules/.pnpm/vitest@1.5.0_@types+node@20.12.7/node_modules/vitest/', + path: '/Users/mock/luco-inc/pnpm-license-exporter/node_modules/.pnpm/vitest@1.5.0_@types+node@20.12.7/node_modules/vitest/', + }]); + expect(await findLicenseDirent('/Users/mock/luco-inc/pnpm-license-exporter/node_modules/.pnpm/vitest@1.5.0_@types+node@20.12.7/node_modules/vitest')).toBe(undefined); +}); \ No newline at end of file diff --git a/src/pnpm/export-license/utils/find-license-dirent.ts b/src/pnpm/export-license/utils/find-license-dirent.ts new file mode 100644 index 0000000..0cbe428 --- /dev/null +++ b/src/pnpm/export-license/utils/find-license-dirent.ts @@ -0,0 +1,7 @@ +import type { Dirent } from 'node:fs'; +import { readdir } from 'node:fs/promises'; + +export async function findLicenseDirent(pnpmPackagePath: string): Promise { + const dirents = await readdir(pnpmPackagePath, { withFileTypes: true }); + return dirents.find((dirent) => dirent.name.toLowerCase().includes('license')); +} \ No newline at end of file diff --git a/src/pnpm/export-license/utils/search-license.util.test.ts b/src/pnpm/export-license/utils/search-license.util.test.ts new file mode 100644 index 0000000..c0a93d1 --- /dev/null +++ b/src/pnpm/export-license/utils/search-license.util.test.ts @@ -0,0 +1,108 @@ +import { mockPnpmPackages } from '../mocks/mock-pnpm-packages'; +import { searchLicense } from './search-license.util'; +import { findLicenseDirent } from './find-license-dirent'; +import { buildExportedPnpmPackageInfo } from './build-exported-pnpm-package-info'; +import { readLicense } from './read-license.util'; +import { Mock } from 'vitest'; +import { nextLicense } from '../../../license-txt'; + +vitest.mock('./find-license-dirent', () => ({ + findLicenseDirent: vitest.fn().mockResolvedValue({ + name: 'LICENSE.md', + parentPath: '/Users/mock/luco-inc/pnpm-license-exporter/node_modules/.pnpm/vitest@1.5.0_@types+node@20.12.7/node_modules/vitest/', + path: '/Users/mock/luco-inc/pnpm-license-exporter/node_modules/.pnpm/vitest@1.5.0_@types+node@20.12.7/node_modules/vitest/' + }) +})); + +vitest.mock('./build-exported-pnpm-package-info', () => ({ + buildExportedPnpmPackageInfo: vitest.fn().mockImplementation(()=> ({ + name: '@ampproject/remapping', + version: '2.2.0', + license: 'Apache-2.0', + author: 'Justin Ridgewell', + homepage: 'https://github.com/ampproject/remapping#readme', + description: 'Remap sequential sourcemaps through transformations to point at the original source code', + })) +})); + +vitest.mock('./read-license.util', () => ({ + readLicense: vitest.fn().mockResolvedValue(undefined) +})); + +test('should return licenses', async () => { + expect(await searchLicense([ + mockPnpmPackages['Apache-2.0'][0], + mockPnpmPackages.MIT[0], + mockPnpmPackages.MIT[1], + ])).toStrictEqual([ + { + name: '@ampproject/remapping', + version: '2.2.0', + license: 'Apache-2.0', + author: 'Justin Ridgewell', + homepage: 'https://github.com/ampproject/remapping#readme', + description: 'Remap sequential sourcemaps through transformations to point at the original source code', + }, + { + name: '@ampproject/remapping', + version: '2.2.0', + license: 'Apache-2.0', + author: 'Justin Ridgewell', + homepage: 'https://github.com/ampproject/remapping#readme', + description: 'Remap sequential sourcemaps through transformations to point at the original source code', + }, + { + name: '@ampproject/remapping', + version: '2.2.0', + license: 'Apache-2.0', + author: 'Justin Ridgewell', + homepage: 'https://github.com/ampproject/remapping#readme', + description: 'Remap sequential sourcemaps through transformations to point at the original source code', + }, + ]) + expect(findLicenseDirent).toHaveBeenCalledTimes(3); + expect(readLicense).toHaveBeenCalledTimes(3); + expect(buildExportedPnpmPackageInfo).toHaveBeenCalledTimes(3); +}); + +test('should using pnpm package in args', async () => { + (readLicense as Mock).mockResolvedValue(nextLicense); + (buildExportedPnpmPackageInfo as Mock).mockResolvedValue({ + name: '@ampproject/remapping', + version: '2.2.0', + license: 'Apache-2.0', + author: 'Justin Ridgewell', + homepage: 'https://github.com/ampproject/remapping#readme', + description: 'Remap sequential sourcemaps through transformations to point at the original source code', + licenseTxt: nextLicense, + }); + + expect(await searchLicense([mockPnpmPackages['Apache-2.0'][0]])).toStrictEqual([ + { + name: '@ampproject/remapping', + version: '2.2.0', + license: 'Apache-2.0', + author: 'Justin Ridgewell', + homepage: 'https://github.com/ampproject/remapping#readme', + description: 'Remap sequential sourcemaps through transformations to point at the original source code', + licenseTxt: nextLicense, + } + ]); + expect(findLicenseDirent).toHaveBeenCalledTimes(1); + expect(findLicenseDirent).toHaveBeenCalledWith('/Users/luco/ghq/sucomado-frontend/node_modules/.pnpm/@ampproject+remapping@2.2.0/node_modules/@ampproject/remapping'); + expect(readLicense).toHaveBeenCalledTimes(1); + expect(readLicense).toHaveBeenCalledWith({ + name: '@ampproject/remapping', + licensePath: '/Users/luco/ghq/sucomado-frontend/node_modules/.pnpm/@ampproject+remapping@2.2.0/node_modules/@ampproject/remapping/LICENSE.md', + }); + expect(buildExportedPnpmPackageInfo).toHaveBeenCalledTimes(1); + expect(buildExportedPnpmPackageInfo).toHaveBeenCalledWith({ + name: '@ampproject/remapping', + version: '2.2.0', + license: 'Apache-2.0', + author: 'Justin Ridgewell', + homepage: 'https://github.com/ampproject/remapping#readme', + description: 'Remap sequential sourcemaps through transformations to point at the original source code', + path: "/Users/luco/ghq/sucomado-frontend/node_modules/.pnpm/@ampproject+remapping@2.2.0/node_modules/@ampproject/remapping" + }, nextLicense); +}) \ No newline at end of file diff --git a/src/pnpm/export-license/utils/search-license.util.ts b/src/pnpm/export-license/utils/search-license.util.ts index 5bf4f79..50c471b 100644 --- a/src/pnpm/export-license/utils/search-license.util.ts +++ b/src/pnpm/export-license/utils/search-license.util.ts @@ -1,12 +1,12 @@ -import { readdir } from 'node:fs/promises'; import type { PnpmPackageInfo, ExportedPnpmPackageInfo } from '../types/export-licenses.types'; import { readLicense } from './read-license.util'; +import { findLicenseDirent } from './find-license-dirent'; +import { buildExportedPnpmPackageInfo } from './build-exported-pnpm-package-info'; -export function searchLicense(packages: PnpmPackageInfo[]) { +export function searchLicense(packages: PnpmPackageInfo[]): Promise<(ExportedPnpmPackageInfo|undefined)[]> { return Promise.all( packages.map(async (pnpmPackage): Promise => { - const dirents = await readdir(pnpmPackage.path, { withFileTypes: true }); - const licenseDirent = dirents.find((dirent) => dirent.name.toLowerCase().includes('license')); + const licenseDirent = await findLicenseDirent(pnpmPackage.path); if (licenseDirent === undefined) { return undefined; } @@ -14,9 +14,7 @@ export function searchLicense(packages: PnpmPackageInfo[]) { name: pnpmPackage.name, licensePath: `${pnpmPackage.path}/${licenseDirent.name}`, }); - // remove path property - const { path, ...PnpmPackageInfo } = pnpmPackage; - return licenseTxt === undefined ? PnpmPackageInfo : { ...PnpmPackageInfo, licenseTxt }; + return buildExportedPnpmPackageInfo(pnpmPackage, licenseTxt); }), ).then((licenses) => licenses.filter(Boolean)); } diff --git a/src/pnpm/export-license/utils/write-licenses.test.ts b/src/pnpm/export-license/utils/write-licenses.test.ts new file mode 100644 index 0000000..200b7bf --- /dev/null +++ b/src/pnpm/export-license/utils/write-licenses.test.ts @@ -0,0 +1,138 @@ +import { writeLicenses } from './write-licenses'; +import { writeFile, mkdir } from 'node:fs/promises'; +import { generateFlatPnpmPackages } from './generate-flat-pnpm-packages.util'; +import { searchLicense } from './search-license.util'; +import path from 'node:path'; +import { mockPnpmPackages } from '../mocks/mock-pnpm-packages'; +import { Mock } from 'vitest'; + +vi.mock('node:path', () => ({ + ...vi.importActual('node:path'), + default: { + dirname: vi.fn().mockReturnValue('src/licenses'), + } +})); +vi.mock('node:fs/promises', () => ({ + ...vi.importActual('node:fs/promises'), + writeFile: vi.fn().mockResolvedValue(undefined), + mkdir: vi.fn().mockResolvedValue(undefined), +})); +vi.mock('./generate-flat-pnpm-packages.util', () => ({ + generateFlatPnpmPackages: vi.fn().mockImplementation(()=> [ + mockPnpmPackages['Apache-2.0'][0], + mockPnpmPackages.MIT[0], + mockPnpmPackages.MIT[1], + ]) +})); +vi.mock('./search-license.util', () => ({ + searchLicense: vi.fn().mockResolvedValue([ + { + name: '@ampproject/remapping', + version: '2.2.0', + license: 'Apache-2.0', + author: 'Justin Ridgewell', + homepage: 'https://github.com/ampproject/remapping#readme', + description: 'Remap sequential sourcemaps through transformations to point at the original source code', + }, + { + name: '@ampproject/remapping', + version: '2.2.0', + license: 'Apache-2.0', + author: 'Justin Ridgewell', + homepage: 'https://github.com/ampproject/remapping#readme', + description: 'Remap sequential sourcemaps through transformations to point at the original source code', + }, + { + name: '@ampproject/remapping', + version: '2.2.0', + license: 'Apache-2.0', + author: 'Justin Ridgewell', + homepage: 'https://github.com/ampproject/remapping#readme', + description: 'Remap sequential sourcemaps through transformations to point at the original source code', + }, + ]), +})); + +test('should write licenses', async () => { + await writeLicenses({}); + expect(generateFlatPnpmPackages).toHaveBeenCalledTimes(1); + expect(generateFlatPnpmPackages).toHaveBeenCalledWith({ + dev: false, + noOptional: false, + noProd: false, + recursive: false, + }); + expect(searchLicense).toHaveBeenCalledTimes(1); + expect(searchLicense).toHaveBeenCalledWith([ + mockPnpmPackages['Apache-2.0'][0], + mockPnpmPackages.MIT[0], + mockPnpmPackages.MIT[1], + ]); + expect(path.dirname).toHaveBeenCalledTimes(1); + expect(path.dirname).toHaveBeenCalledWith('./licenses.json'); + expect(mkdir).toHaveBeenCalledTimes(1); + expect(mkdir).toHaveBeenCalledWith('src/licenses', { recursive: true }); + expect(writeFile).toHaveBeenCalledTimes(1); + expect(writeFile).toHaveBeenCalledWith('./licenses.json', JSON.stringify([ + { + name: '@ampproject/remapping', + version: '2.2.0', + license: 'Apache-2.0', + author: 'Justin Ridgewell', + homepage: 'https://github.com/ampproject/remapping#readme', + description: 'Remap sequential sourcemaps through transformations to point at the original source code', + }, + { + name: '@ampproject/remapping', + version: '2.2.0', + license: 'Apache-2.0', + author: 'Justin Ridgewell', + homepage: 'https://github.com/ampproject/remapping#readme', + description: 'Remap sequential sourcemaps through transformations to point at the original source code', + }, + { + name: '@ampproject/remapping', + version: '2.2.0', + license: 'Apache-2.0', + author: 'Justin Ridgewell', + homepage: 'https://github.com/ampproject/remapping#readme', + description: 'Remap sequential sourcemaps through transformations to point at the original source code', + }, + ], undefined, undefined)); +}); + +test('should not a make directory', async () => { + (path.dirname as Mock).mockReturnValue(''); + await writeLicenses({}); + expect(mkdir).toHaveBeenCalledTimes(0); +}) + +test('should pretty', async () => { + await writeLicenses({ pretty: true }); + expect(writeFile).toHaveBeenCalledWith('./licenses.json', JSON.stringify([ + { + name: '@ampproject/remapping', + version: '2.2.0', + license: 'Apache-2.0', + author: 'Justin Ridgewell', + homepage: 'https://github.com/ampproject/remapping#readme', + description: 'Remap sequential sourcemaps through transformations to point at the original source code', + }, + { + name: '@ampproject/remapping', + version: '2.2.0', + license: 'Apache-2.0', + author: 'Justin Ridgewell', + homepage: 'https://github.com/ampproject/remapping#readme', + description: 'Remap sequential sourcemaps through transformations to point at the original source code', + }, + { + name: '@ampproject/remapping', + version: '2.2.0', + license: 'Apache-2.0', + author: 'Justin Ridgewell', + homepage: 'https://github.com/ampproject/remapping#readme', + description: 'Remap sequential sourcemaps through transformations to point at the original source code', + }, + ], undefined, 2)); +}) \ No newline at end of file diff --git a/src/pnpm/export-license/utils/write-licenses.ts b/src/pnpm/export-license/utils/write-licenses.ts index b8d03ae..5d2a16e 100644 --- a/src/pnpm/export-license/utils/write-licenses.ts +++ b/src/pnpm/export-license/utils/write-licenses.ts @@ -2,7 +2,7 @@ import { writeFile, mkdir } from 'node:fs/promises'; import { generateFlatPnpmPackages } from './generate-flat-pnpm-packages.util'; import { searchLicense } from './search-license.util'; -import { argsOptions } from '../../../bin/pnpm-license-exporter'; +import { argsOptions } from '../../../bin/args-options'; import path from 'node:path'; type argType = { diff --git a/vitest.config.ts b/vitest.config.ts index ce5a2a6..8e63989 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -7,12 +7,13 @@ export default defineConfig({ enabled: true, provider: 'v8', thresholds: { - statements: 76.66, - branches: 50, - functions: 25, - lines: 76.66, + statements: 98.74, + branches: 81.25, + functions: 100, + lines: 98.74, autoUpdate: true, }, + exclude: ['**/*/index.ts', '**/*/*.types.ts', '**/*/*.d.ts', '**/*/*.js','*.cjs', 'apps', 'src/bin', '**/const/*.*'], }, clearMocks: true, },