From 6fd86edb32a90922d86a42f9c15b3672a64bdf6b Mon Sep 17 00:00:00 2001 From: Matthew Vaughan <mattvaughan38@gmail.com> Date: Thu, 7 Apr 2022 15:45:24 -0600 Subject: [PATCH] feat(manager/nuget): Restore all dependent project files to generate new lock files (#14312) Co-authored-by: Michael Kriese <michael.kriese@visualon.de> --- .../circular-reference/one/one.csproj | 16 +++ .../circular-reference/two/two.csproj | 16 +++ .../single-project-file/single.csproj | 12 ++ .../__fixtures__/two-no-reference/one.csproj | 20 ++++ .../__fixtures__/two-no-reference/two.csproj | 12 ++ .../two-one-reference/one/one.csproj | 12 ++ .../two-one-reference/one/packages.lock.json | 13 +++ .../two-one-reference/two/packages.lock.json | 19 ++++ .../two-one-reference/two/two.csproj | 16 +++ lib/modules/manager/nuget/artifacts.spec.ts | 4 +- lib/modules/manager/nuget/artifacts.ts | 86 ++++++++++---- .../manager/nuget/package-tree.spec.ts | 82 ++++++++++++++ lib/modules/manager/nuget/package-tree.ts | 107 ++++++++++++++++++ package.json | 1 + tsconfig.strict.json | 1 + yarn.lock | 6 +- 16 files changed, 398 insertions(+), 25 deletions(-) create mode 100644 lib/modules/manager/nuget/__fixtures__/circular-reference/one/one.csproj create mode 100644 lib/modules/manager/nuget/__fixtures__/circular-reference/two/two.csproj create mode 100644 lib/modules/manager/nuget/__fixtures__/single-project-file/single.csproj create mode 100644 lib/modules/manager/nuget/__fixtures__/two-no-reference/one.csproj create mode 100644 lib/modules/manager/nuget/__fixtures__/two-no-reference/two.csproj create mode 100644 lib/modules/manager/nuget/__fixtures__/two-one-reference/one/one.csproj create mode 100644 lib/modules/manager/nuget/__fixtures__/two-one-reference/one/packages.lock.json create mode 100644 lib/modules/manager/nuget/__fixtures__/two-one-reference/two/packages.lock.json create mode 100644 lib/modules/manager/nuget/__fixtures__/two-one-reference/two/two.csproj create mode 100644 lib/modules/manager/nuget/package-tree.spec.ts create mode 100644 lib/modules/manager/nuget/package-tree.ts diff --git a/lib/modules/manager/nuget/__fixtures__/circular-reference/one/one.csproj b/lib/modules/manager/nuget/__fixtures__/circular-reference/one/one.csproj new file mode 100644 index 0000000000..3bb51c6cdb --- /dev/null +++ b/lib/modules/manager/nuget/__fixtures__/circular-reference/one/one.csproj @@ -0,0 +1,16 @@ +<Project Sdk="Microsoft.NET.Sdk.Web"> + + <PropertyGroup> + <TargetFramework>net5.0</TargetFramework> + <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Serilog" Version="2.10.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="../two/two.csproj" /> + </ItemGroup> + +</Project> diff --git a/lib/modules/manager/nuget/__fixtures__/circular-reference/two/two.csproj b/lib/modules/manager/nuget/__fixtures__/circular-reference/two/two.csproj new file mode 100644 index 0000000000..0500818314 --- /dev/null +++ b/lib/modules/manager/nuget/__fixtures__/circular-reference/two/two.csproj @@ -0,0 +1,16 @@ +<Project Sdk="Microsoft.NET.Sdk.Web"> + + <PropertyGroup> + <TargetFramework>net5.0</TargetFramework> + <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Serilog" Version="2.10.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="../one/one.csproj" /> + </ItemGroup> + +</Project> diff --git a/lib/modules/manager/nuget/__fixtures__/single-project-file/single.csproj b/lib/modules/manager/nuget/__fixtures__/single-project-file/single.csproj new file mode 100644 index 0000000000..8edcb2d22f --- /dev/null +++ b/lib/modules/manager/nuget/__fixtures__/single-project-file/single.csproj @@ -0,0 +1,12 @@ +<Project Sdk="Microsoft.NET.Sdk.Web"> + + <PropertyGroup> + <TargetFramework>net5.0</TargetFramework> + <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Serilog" Version="2.10.0" /> + </ItemGroup> + +</Project> diff --git a/lib/modules/manager/nuget/__fixtures__/two-no-reference/one.csproj b/lib/modules/manager/nuget/__fixtures__/two-no-reference/one.csproj new file mode 100644 index 0000000000..836c7a6433 --- /dev/null +++ b/lib/modules/manager/nuget/__fixtures__/two-no-reference/one.csproj @@ -0,0 +1,20 @@ +<Project Sdk="Microsoft.NET.Sdk.Web"> + + <PropertyGroup> + <TargetFramework>net5.0</TargetFramework> + <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> + <DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Serilog" Version="2.10.0" /> + <PackageReference Include="Serilog.Formatting.Compact" Version="1.1.0" /> + <PackageReference Include="Serilog.Exceptions" Version="5.6.0" /> + <PackageReference Include="Serilog.Extensions.Hosting" Version="4.1.2" /> + <PackageReference Include="Serilog.Enrichers.Environment" Version="2.1.3" /> + <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" /> + <PackageReference Include="serilog.sinks.graylog" Version="2.3.0" /> + <PackageReference Include="Swashbuckle.AspNetCore" Version="6.1.3" /> + </ItemGroup> + +</Project> diff --git a/lib/modules/manager/nuget/__fixtures__/two-no-reference/two.csproj b/lib/modules/manager/nuget/__fixtures__/two-no-reference/two.csproj new file mode 100644 index 0000000000..8edcb2d22f --- /dev/null +++ b/lib/modules/manager/nuget/__fixtures__/two-no-reference/two.csproj @@ -0,0 +1,12 @@ +<Project Sdk="Microsoft.NET.Sdk.Web"> + + <PropertyGroup> + <TargetFramework>net5.0</TargetFramework> + <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Serilog" Version="2.10.0" /> + </ItemGroup> + +</Project> diff --git a/lib/modules/manager/nuget/__fixtures__/two-one-reference/one/one.csproj b/lib/modules/manager/nuget/__fixtures__/two-one-reference/one/one.csproj new file mode 100644 index 0000000000..8edcb2d22f --- /dev/null +++ b/lib/modules/manager/nuget/__fixtures__/two-one-reference/one/one.csproj @@ -0,0 +1,12 @@ +<Project Sdk="Microsoft.NET.Sdk.Web"> + + <PropertyGroup> + <TargetFramework>net5.0</TargetFramework> + <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Serilog" Version="2.10.0" /> + </ItemGroup> + +</Project> diff --git a/lib/modules/manager/nuget/__fixtures__/two-one-reference/one/packages.lock.json b/lib/modules/manager/nuget/__fixtures__/two-one-reference/one/packages.lock.json new file mode 100644 index 0000000000..374c404b2d --- /dev/null +++ b/lib/modules/manager/nuget/__fixtures__/two-one-reference/one/packages.lock.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "dependencies": { + ".NETCoreApp,Version=v5.0": { + "Serilog": { + "type": "Direct", + "requested": "[2.10.0, )", + "resolved": "2.10.0", + "contentHash": "+QX0hmf37a0/OZLxM3wL7V6/ADvC1XihXN4Kq/p6d8lCPfgkRdiuhbWlMaFjR9Av0dy5F0+MBeDmDdRZN/YwQA==" + } + } + } +} \ No newline at end of file diff --git a/lib/modules/manager/nuget/__fixtures__/two-one-reference/two/packages.lock.json b/lib/modules/manager/nuget/__fixtures__/two-one-reference/two/packages.lock.json new file mode 100644 index 0000000000..426b0a04bf --- /dev/null +++ b/lib/modules/manager/nuget/__fixtures__/two-one-reference/two/packages.lock.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "dependencies": { + ".NETCoreApp,Version=v5.0": { + "Serilog": { + "type": "Direct", + "requested": "[2.10.0, )", + "resolved": "2.10.0", + "contentHash": "+QX0hmf37a0/OZLxM3wL7V6/ADvC1XihXN4Kq/p6d8lCPfgkRdiuhbWlMaFjR9Av0dy5F0+MBeDmDdRZN/YwQA==" + }, + "one": { + "type": "Project", + "dependencies": { + "Serilog": "2.10.0" + } + } + } + } +} \ No newline at end of file diff --git a/lib/modules/manager/nuget/__fixtures__/two-one-reference/two/two.csproj b/lib/modules/manager/nuget/__fixtures__/two-one-reference/two/two.csproj new file mode 100644 index 0000000000..0500818314 --- /dev/null +++ b/lib/modules/manager/nuget/__fixtures__/two-one-reference/two/two.csproj @@ -0,0 +1,16 @@ +<Project Sdk="Microsoft.NET.Sdk.Web"> + + <PropertyGroup> + <TargetFramework>net5.0</TargetFramework> + <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Serilog" Version="2.10.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="../one/one.csproj" /> + </ItemGroup> + +</Project> diff --git a/lib/modules/manager/nuget/artifacts.spec.ts b/lib/modules/manager/nuget/artifacts.spec.ts index e46ffc0b7a..3d9d356bf7 100644 --- a/lib/modules/manager/nuget/artifacts.spec.ts +++ b/lib/modules/manager/nuget/artifacts.spec.ts @@ -1,7 +1,7 @@ import { exec as _exec } from 'child_process'; import { join } from 'upath'; import { envMock, mockExecAll } from '../../../../test/exec-util'; -import { fs, mocked } from '../../../../test/util'; +import { fs, git, mocked } from '../../../../test/util'; import { GlobalConfig } from '../../../config/global'; import type { RepoGlobalConfig } from '../../../config/types'; import * as docker from '../../../util/exec/docker'; @@ -19,6 +19,7 @@ jest.mock('child_process'); jest.mock('../../../util/exec/env'); jest.mock('../../../util/fs'); jest.mock('../../../util/host-rules'); +jest.mock('../../../util/git'); jest.mock('./util'); const exec: jest.Mock<typeof _exec> = _exec as any; @@ -48,6 +49,7 @@ describe('modules/manager/nuget/artifacts', () => { fs.ensureCacheDir.mockImplementation((dirName: string) => Promise.resolve(`others/${dirName}`) ); + git.getFileList.mockResolvedValueOnce([]); getRandomString.mockReturnValue('not-so-random' as any); GlobalConfig.set(adminConfig); docker.resetPrefetchedImages(); diff --git a/lib/modules/manager/nuget/artifacts.ts b/lib/modules/manager/nuget/artifacts.ts index 4467624cd5..fc73500dec 100644 --- a/lib/modules/manager/nuget/artifacts.ts +++ b/lib/modules/manager/nuget/artifacts.ts @@ -21,6 +21,7 @@ import type { UpdateArtifactsConfig, UpdateArtifactsResult, } from '../types'; +import { getDependentPackageFiles } from './package-tree'; import { getConfiguredRegistries, getDefaultRegistries, @@ -57,6 +58,7 @@ async function addSourceCmds( async function runDotnetRestore( packageFileName: string, + dependentPackageFileNames: string[], config: UpdateArtifactsConfig ): Promise<void> { const execOptions: ExecOptions = { @@ -72,15 +74,33 @@ async function runDotnetRestore( nugetConfigFile, `<?xml version="1.0" encoding="utf-8"?>\n<configuration>\n</configuration>\n` ); + const cmds = [ ...(await addSourceCmds(packageFileName, config, nugetConfigFile)), - `dotnet restore ${packageFileName} --force-evaluate --configfile ${nugetConfigFile}`, + ...dependentPackageFileNames.map( + (f) => + `dotnet restore ${f} --force-evaluate --configfile ${nugetConfigFile}` + ), ]; - logger.debug({ cmd: cmds }, 'dotnet command'); await exec(cmds, execOptions); await remove(nugetConfigDir); } +async function getLockFileContentMap( + lockFileNames: string[] +): Promise<Record<string, string>> { + const lockFileContentMap: Record<string, string> = {}; + + for (const lockFileName of lockFileNames) { + lockFileContentMap[lockFileName] = await readLocalFile( + lockFileName, + 'utf8' + ); + } + + return lockFileContentMap; +} + export async function updateArtifacts({ packageFileName, newPackageFileContent, @@ -101,15 +121,29 @@ export async function updateArtifacts({ return null; } - const lockFileName = getSiblingFileName( + const packageFiles = [ + ...(await getDependentPackageFiles(packageFileName)), packageFileName, - 'packages.lock.json' + ]; + + logger.trace( + { packageFiles }, + `Found ${packageFiles.length} dependent package files` + ); + + const lockFileNames = packageFiles.map((f) => + getSiblingFileName(f, 'packages.lock.json') ); - const existingLockFileContent = await readLocalFile(lockFileName, 'utf8'); - if (!existingLockFileContent) { + + const existingLockFileContentMap = await getLockFileContentMap(lockFileNames); + + const hasLockFileContent = Object.values(existingLockFileContentMap).some( + (val) => !!val + ); + if (!hasLockFileContent) { logger.debug( { packageFileName }, - 'No lock file found beneath package file.' + 'No lock file found for package or dependents' ); return null; } @@ -124,23 +158,29 @@ export async function updateArtifacts({ await writeLocalFile(packageFileName, newPackageFileContent); - await runDotnetRestore(packageFileName, config); + await runDotnetRestore(packageFileName, packageFiles, config); - const newLockFileContent = await readLocalFile(lockFileName, 'utf8'); - if (existingLockFileContent === newLockFileContent) { - logger.debug(`Lock file is unchanged`); - return null; + const newLockFileContentMap = await getLockFileContentMap(lockFileNames); + + const retArray = []; + for (const lockFileName of lockFileNames) { + if ( + existingLockFileContentMap[lockFileName] === + newLockFileContentMap[lockFileName] + ) { + logger.trace(`Lock file ${lockFileName} is unchanged`); + } else { + retArray.push({ + file: { + type: 'addition', + path: lockFileName, + contents: newLockFileContentMap[lockFileName], + }, + }); + } } - logger.debug('Returning updated lock file'); - return [ - { - file: { - type: 'addition', - path: lockFileName, - contents: await readLocalFile(lockFileName), - }, - }, - ]; + + return retArray.length > 0 ? retArray : null; } catch (err) { // istanbul ignore if if (err.message === TEMPORARY_ERROR) { @@ -150,7 +190,7 @@ export async function updateArtifacts({ return [ { artifactError: { - lockFile: lockFileName, + lockFile: lockFileNames.join(', '), stderr: err.message, }, }, diff --git a/lib/modules/manager/nuget/package-tree.spec.ts b/lib/modules/manager/nuget/package-tree.spec.ts new file mode 100644 index 0000000000..2e494b59d7 --- /dev/null +++ b/lib/modules/manager/nuget/package-tree.spec.ts @@ -0,0 +1,82 @@ +import { fs as memfs } from 'memfs'; +import upath from 'upath'; +import { Fixtures } from '../../../../test/fixtures'; +import { git } from '../../../../test/util'; +import { GlobalConfig } from '../../../config/global'; +import type { RepoGlobalConfig } from '../../../config/types'; +import { getDependentPackageFiles } from './package-tree'; + +jest.mock('fs', () => memfs); +jest.mock('fs-extra', () => Fixtures.fsExtra()); +jest.mock('../../../util/git'); + +const adminConfig: RepoGlobalConfig = { + localDir: upath.resolve('/tmp/repo'), +}; + +describe('modules/manager/nuget/package-tree', () => { + describe('getDependentPackageFiles()', () => { + beforeEach(() => { + GlobalConfig.set(adminConfig); + Fixtures.reset(); + }); + + afterEach(() => { + GlobalConfig.reset(); + Fixtures.reset(); + }); + + it('returns empty list for single project', async () => { + git.getFileList.mockResolvedValue(['single.csproj']); + Fixtures.mock({ + '/tmp/repo/single.csproj': Fixtures.get( + 'single-project-file/single.csproj' + ), + }); + + expect(await getDependentPackageFiles('single.csproj')).toBeEmpty(); + }); + + it('returns empty list for two projects with no references', async () => { + git.getFileList.mockResolvedValue(['one.csproj', 'two.csproj']); + Fixtures.mock({ + '/tmp/repo/one.csproj': Fixtures.get('two-no-reference/one.csproj'), + '/tmp/repo/two.csproj': Fixtures.get('two-no-reference/two.csproj'), + }); + + expect(await getDependentPackageFiles('one.csproj')).toBeEmpty(); + }); + + it('returns project for two projects with one reference', async () => { + git.getFileList.mockResolvedValue(['one/one.csproj', 'two/two.csproj']); + Fixtures.mock({ + '/tmp/repo/one/one.csproj': Fixtures.get( + 'two-one-reference/one/one.csproj' + ), + '/tmp/repo/two/two.csproj': Fixtures.get( + 'two-one-reference/two/two.csproj' + ), + }); + + expect(await getDependentPackageFiles('one/one.csproj')).toEqual([ + 'two/two.csproj', + ]); + }); + + it('throws error on circular reference', async () => { + git.getFileList.mockResolvedValue(['one/one.csproj', 'two/two.csproj']); + Fixtures.mock({ + '/tmp/repo/one/one.csproj': Fixtures.get( + 'circular-reference/one/one.csproj' + ), + '/tmp/repo/two/two.csproj': Fixtures.get( + 'circular-reference/two/two.csproj' + ), + }); + + await expect(getDependentPackageFiles('one/one.csproj')).rejects.toThrow( + 'Circular reference detected in NuGet package files' + ); + }); + }); +}); diff --git a/lib/modules/manager/nuget/package-tree.ts b/lib/modules/manager/nuget/package-tree.ts new file mode 100644 index 0000000000..6d697d11e4 --- /dev/null +++ b/lib/modules/manager/nuget/package-tree.ts @@ -0,0 +1,107 @@ +import Graph from 'graph-data-structure'; +import minimatch from 'minimatch'; +import upath from 'upath'; +import xmldoc from 'xmldoc'; +import { logger } from '../../../logger'; +import { readLocalFile } from '../../../util/fs'; +import { getFileList } from '../../../util/git'; + +/** + * Get all package files at any level of ancestry that depend on packageFileName + */ +export async function getDependentPackageFiles( + packageFileName: string +): Promise<string[]> { + const packageFiles = await getAllPackageFiles(); + const graph: ReturnType<typeof Graph> = Graph(); + + for (const f of packageFiles) { + graph.addNode(f); + } + + for (const f of packageFiles) { + const packageFileContent = (await readLocalFile(f, 'utf8')).toString(); + + const doc = new xmldoc.XmlDocument(packageFileContent); + const projectReferenceAttributes = ( + doc + .childrenNamed('ItemGroup') + .map((ig) => ig.childrenNamed('ProjectReference')) ?? [] + ) + .flat() + .map((pf) => pf.attr['Include']); + + const projectReferences = projectReferenceAttributes.map((a) => + upath.normalize(a) + ); + const normalizedRelativeProjectReferences = projectReferences.map((r) => + reframeRelativePathToRootOfRepo(f, r) + ); + + for (const ref of normalizedRelativeProjectReferences) { + graph.addEdge(ref, f); + } + + if (graph.hasCycle()) { + throw new Error('Circular reference detected in NuGet package files'); + } + } + + return recursivelyGetDependentPackageFiles(packageFileName, graph); +} + +/** + * Traverse graph and find dependent package files at any level of ancestry + */ +function recursivelyGetDependentPackageFiles( + packageFileName: string, + graph: ReturnType<typeof Graph> +): string[] { + const dependents: string[] = graph.adjacent(packageFileName); + + if (dependents.length === 0) { + return []; + } + + return dependents.concat( + dependents.map((d) => recursivelyGetDependentPackageFiles(d, graph)).flat() + ); +} + +/** + * Take the path relative from a project file, and make it relative from the root of the repo + */ +function reframeRelativePathToRootOfRepo( + dependentProjectRelativePath: string, + projectReference: string +): string { + const virtualRepoRoot = '/'; + const absoluteDependentProjectPath = upath.resolve( + virtualRepoRoot, + dependentProjectRelativePath + ); + const absoluteProjectReferencePath = upath.resolve( + upath.dirname(absoluteDependentProjectPath), + projectReference + ); + const relativeProjectReferencePath = upath.relative( + virtualRepoRoot, + absoluteProjectReferencePath + ); + + return relativeProjectReferencePath; +} + +/** + * Get a list of package files in localDir + */ +async function getAllPackageFiles(): Promise<string[]> { + const allFiles = await getFileList(); + const filteredPackageFiles = allFiles.filter( + minimatch.filter('*.{cs,vb,fs}proj', { matchBase: true, nocase: true }) + ); + + logger.trace({ filteredPackageFiles }, 'Found package files'); + + return filteredPackageFiles; +} diff --git a/package.json b/package.json index 323540b7f0..e63c10a6d2 100644 --- a/package.json +++ b/package.json @@ -168,6 +168,7 @@ "global-agent": "3.0.0", "good-enough-parser": "1.1.7", "got": "11.8.3", + "graph-data-structure": "2.0.0", "handlebars": "4.7.7", "hasha": "5.2.2", "ignore": "5.2.0", diff --git a/tsconfig.strict.json b/tsconfig.strict.json index 5d902abee5..0270c8bab8 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -241,6 +241,7 @@ "lib/modules/manager/nuget/extract.ts", "lib/modules/manager/nuget/extract/global-manifest.ts", "lib/modules/manager/nuget/index.ts", + "lib/modules/manager/nuget/package-tree.ts", "lib/modules/manager/nuget/util.ts", "lib/modules/manager/nvm/extract.ts", "lib/modules/manager/nvm/index.ts", diff --git a/yarn.lock b/yarn.lock index 2a664f1ea2..7a542e5433 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5066,6 +5066,11 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== +graph-data-structure@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/graph-data-structure/-/graph-data-structure-2.0.0.tgz#a95762a067f81f0fe4ea77525253b946a941fa89" + integrity sha512-ravWDe9LaV0u27ZDme/8w5xHyyTqIWQsetpzDvBNJsGy4nZQB5IVw/u+7ngdEMZWsHYim+PAHatV4cGQX1XKpQ== + grapheme-splitter@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" @@ -5168,7 +5173,6 @@ hosted-git-info@^5.0.0: integrity sha512-rRnjWu0Bxj+nIfUOkz0695C0H6tRrN5iYIzYejb0tDEefe2AekHu/U5Kn9pEie5vsJqpNQU02az7TGSH3qpz4Q== dependencies: lru-cache "^7.5.1" - html-encoding-sniffer@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" -- GitLab