From a1075e65e63b399119e421c6435f470f98b1113e Mon Sep 17 00:00:00 2001 From: Jannik Wibker Date: Wed, 28 Feb 2024 18:09:53 +0100 Subject: [PATCH] add support for new schema used by pnpm 9 --- package.json | 2 +- readme.md | 2 ++ src/get-dependencies.ts | 46 +++++++++++++++++++++++++++-- src/get-license-text.ts | 8 ++--- src/index.ts | 16 +++++----- src/resolve-licenses-best-effort.ts | 8 ++--- 6 files changed, 61 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index ada3567..801b189 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@quantco/pnpm-licenses", - "version": "1.0.3", + "version": "2.0.0", "description": "Generate third party license disclaimers in pnpm-based projects", "homepage": "https://github.com/Quantco/pnpm-licenses", "repository": { diff --git a/readme.md b/readme.md index d62479f..0f04fdd 100644 --- a/readme.md +++ b/readme.md @@ -83,6 +83,8 @@ type Dependency = { } ``` +> Note that if multiple versions of a package are installed the outut will contain the same package multiple times with differing versions (and paths) + ### Options ``` diff --git a/src/get-dependencies.ts b/src/get-dependencies.ts index b505464..b1a4c81 100644 --- a/src/get-dependencies.ts +++ b/src/get-dependencies.ts @@ -2,7 +2,17 @@ import fs from 'fs/promises' import { exec } from 'child_process' import z from 'zod' -const pnpmDependencySchema = z.object({ +const pnpmDependencyGroupedSchema = z.object({ + name: z.string(), + versions: z.array(z.string()), + paths: z.array(z.string()), + license: z.string(), + author: z.string().optional(), + homepage: z.string().optional(), + description: z.string().optional() +}) + +const pnpmDependencyFlattenedSchema = z.object({ name: z.string(), version: z.string(), path: z.string(), @@ -12,11 +22,33 @@ const pnpmDependencySchema = z.object({ description: z.string().optional() }) +const pnpmDependencySchema = z.union([pnpmDependencyGroupedSchema, pnpmDependencyFlattenedSchema]) + const pnpmInputSchema = z.record(z.string(), z.array(pnpmDependencySchema)) export type PnpmDependency = z.infer export type PnpmJson = z.infer +export type PnpmDependencyFlattened = z.infer + +export const flattenDependencies = (deps: PnpmDependency[]): PnpmDependencyFlattened[] => + deps.flatMap(({ name, license, author, homepage, description, ...rest }) => { + if ('version' in rest) { + return [{ name, license, author, homepage, description, ...rest }] + } else { + const { versions, paths } = rest + return versions.map((version, i) => ({ + name, + version, + path: paths[i], + license, + author, + homepage, + description + })) + } + }) + async function read(stream: NodeJS.ReadableStream) { const chunks: Buffer[] = [] for await (const chunk of stream) { @@ -32,7 +64,10 @@ export type IOOptions = ( ) & ({ stdout: true; outputFile: undefined } | { stdout: false; outputFile: string }) -export const getDependencies = (options: { prod: boolean }, ioOptions: IOOptions): Promise => { +export const getDependencies = ( + options: { prod: boolean }, + ioOptions: IOOptions +): Promise => { let inputPromise: Promise | undefined if (ioOptions.stdin) { @@ -49,5 +84,10 @@ export const getDependencies = (options: { prod: boolean }, ioOptions: IOOptions } // TODO: proper error handling pls - return inputPromise.then(JSON.parse).then(pnpmInputSchema.parse) + return inputPromise + .then(JSON.parse) + .then(pnpmInputSchema.parse) + .then(Object.values) + .then((deps: PnpmDependency[][]) => deps.flat()) + .then(flattenDependencies) } diff --git a/src/get-license-text.ts b/src/get-license-text.ts index fe332ab..808a999 100644 --- a/src/get-license-text.ts +++ b/src/get-license-text.ts @@ -3,7 +3,7 @@ import path from 'path' import licenseTexts from 'spdx-license-list/full.js' import stripIndent from 'strip-indent' import removeMarkdown from 'remove-markdown' -import type { PnpmDependency } from './get-dependencies' +import type { PnpmDependencyFlattened } from './get-dependencies' const LICENSE_BASENAMES = [/* eslint-disable prettier/prettier */ /^LICENSE$/i, // e.g. LICENSE @@ -36,7 +36,7 @@ const LICENSE_TEXT_SUBSTRINGS = { } export class MissingLicenseError extends Error { - constructor(public dependency: PnpmDependency) { + constructor(public dependency: PnpmDependencyFlattened) { super('No license text found for dependency ' + dependency.name) } } @@ -50,14 +50,14 @@ const prettifyLicenseText = (licenseText: string) => { return stripIndent(removeMarkdown(licenseText)).trim() } -export type PnpmDependencyResolvedLicenseText = PnpmDependency & { +export type PnpmDependencyResolvedLicenseText = PnpmDependencyFlattened & { licenseText: string additionalText?: string resolvedBy: (typeof resolvedByTypes)[number] } export const getLicenseText = async ( - dependency: PnpmDependency + dependency: PnpmDependencyFlattened ): Promise<{ licenseText: string; additionalText?: string; resolvedBy: (typeof resolvedByTypes)[number] }> => { const files = await fs.readdir(dependency.path) diff --git a/src/index.ts b/src/index.ts index 2c02d25..9494cce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import fs from 'fs/promises' import multimatch from 'multimatch' import { getDependencies } from './get-dependencies' -import type { PnpmDependency } from './get-dependencies' +import type { PnpmDependencyFlattened } from './get-dependencies' import { generateDisclaimer } from './generate-disclaimer' import { resolveLicensesBestEffort } from './resolve-licenses-best-effort' @@ -31,10 +31,9 @@ const output = (value: string, options: IOOptions) => { } export const listCommand = async (options: ListOptions, ioOptions: IOOptions) => { - const deps = getDependencies(options, ioOptions) - .then(Object.values) - .then((deps: PnpmDependency[][]) => deps.flat()) - .then((deps: PnpmDependency[]) => deps.filter((dep) => multimatch(dep.name, options.filters).length === 0)) + const deps = getDependencies(options, ioOptions).then((deps: PnpmDependencyFlattened[]) => + deps.filter((dep) => multimatch(dep.name, options.filters).length === 0) + ) const { successful, failed } = await resolveLicensesBestEffort(await deps) @@ -43,10 +42,9 @@ export const listCommand = async (options: ListOptions, ioOptions: IOOptions) => } export const generateDisclaimerCommand = async (options: GenerateDisclaimerOptions, ioOptions: IOOptions) => { - const deps = getDependencies(options, ioOptions) - .then(Object.values) - .then((deps: PnpmDependency[][]) => deps.flat()) - .then((deps: PnpmDependency[]) => deps.filter((dep) => multimatch(dep.name, options.filters).length === 0)) + const deps = getDependencies(options, ioOptions).then((deps: PnpmDependencyFlattened[]) => + deps.filter((dep) => multimatch(dep.name, options.filters).length === 0) + ) const { successful, failed } = await resolveLicensesBestEffort(await deps) diff --git a/src/resolve-licenses-best-effort.ts b/src/resolve-licenses-best-effort.ts index 95af0bf..b773bc1 100644 --- a/src/resolve-licenses-best-effort.ts +++ b/src/resolve-licenses-best-effort.ts @@ -1,16 +1,16 @@ -import type { PnpmDependency } from './get-dependencies' +import type { PnpmDependencyFlattened } from './get-dependencies' import { getLicenseText } from './get-license-text' import type { PnpmDependencyResolvedLicenseText } from './get-license-text' export const resolveLicensesBestEffort = async ( - deps: PnpmDependency[] -): Promise<{ successful: PnpmDependencyResolvedLicenseText[]; failed: PnpmDependency[] }> => { + deps: PnpmDependencyFlattened[] +): Promise<{ successful: PnpmDependencyResolvedLicenseText[]; failed: PnpmDependencyFlattened[] }> => { const depsWithLicensesPromise = deps.map(async (dep) => ({ ...dep, ...(await getLicenseText(dep)) })) // keep track of which licenses could be resolved and which couldn't // include the index to restore the original order afterwards const successful: [number, PnpmDependencyResolvedLicenseText][] = [] - const failed: [number, PnpmDependency][] = [] + const failed: [number, PnpmDependencyFlattened][] = [] await Promise.all( depsWithLicensesPromise.map((depPromise, index) =>