diff --git a/.circleci/config.yml b/.circleci/config.yml index eb643a6e..3b4e5924 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,10 +6,6 @@ orbs: jobs: test: - environment: - # prevent Wine popup dialogs about installing additional packages - WINEDLLOVERRIDES: mscoree,mshtml= - WINEDEBUG: -all executor: <> parameters: executor: diff --git a/README.md b/README.md index e8fc49b1..77a00226 100644 --- a/README.md +++ b/README.md @@ -61,15 +61,6 @@ npm install --save-dev @electron/packager It is **not** recommended to install `@electron/packager` globally. -### Building Windows apps from non-Windows platforms - -Building an Electron app for the Windows target platform requires editing the `Electron.exe` file. -Currently, Electron Packager uses [`node-rcedit`](https://github.com/electron/node-rcedit) to accomplish -this. A Windows executable is bundled in that Node package and needs to be run in order for this -functionality to work, so on non-Windows host platforms (not including WSL), -[Wine](https://www.winehq.org/) 1.6 or later needs to be installed. On macOS, it is installable -via [Homebrew](https://brew.sh/). - ## Usage ### Via JavaScript diff --git a/package.json b/package.json index 622488d6..101b65b2 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "@electron/osx-sign": "^1.0.5", "@electron/universal": "^2.0.1", "@electron/windows-sign": "^1.0.0", - "cross-spawn-windows-exe": "^1.2.0", "debug": "^4.0.1", "extract-zip": "^2.0.0", "filenamify": "^4.1.0", @@ -42,7 +41,7 @@ "junk": "^3.1.0", "parse-author": "^2.0.0", "plist": "^3.0.0", - "rcedit": "^4.0.0", + "resedit": "^2.0.0", "resolve": "^1.1.6", "semver": "^7.1.3", "yargs-parser": "^21.1.1" @@ -66,6 +65,7 @@ "eslint-plugin-import": "^2.22.1", "eslint-plugin-node": "^11.0.0", "eslint-plugin-promise": "^5.1.0", + "lodash": "^4.17.21", "nyc": "^15.0.0", "pkg-up": "^4.0.0", "sinon": "^17.0.0", diff --git a/src/resedit.ts b/src/resedit.ts new file mode 100644 index 00000000..36aed675 --- /dev/null +++ b/src/resedit.ts @@ -0,0 +1,118 @@ +import * as fs from 'fs-extra'; +// eslint-disable-next-line import/no-unresolved +import { load as loadResEdit } from 'resedit/cjs'; +import { Win32MetadataOptions } from './types'; + +export type ExeMetadata = { + productVersion?: string; + fileVersion?: string; + legalCopyright?: string; + productName?: string; + iconPath?: string; + win32Metadata?: Win32MetadataOptions; +} + +type ParsedVersionNumerics = [number, number, number, number]; + +/** + * Parse a version string in the format a.b.c.d with each component being optional + * but if present must be an integer. Matches the impl in rcedit for compat + */ +function parseVersionString(str: string): ParsedVersionNumerics { + const parts = str.split('.'); + if (parts.length === 0 || parts.length > 4) { + throw new Error(`Incorrectly formatted version string: "${str}". Should have at least one and at most four components`); + } + return parts.map((part) => { + const parsed = parseInt(part, 10); + if (isNaN(parsed)) { + throw new Error(`Incorrectly formatted version string: "${str}". Component "${part}" could not be parsed as an integer`); + } + return parsed; + }) as ParsedVersionNumerics; +} + +// Ref: https://learn.microsoft.com/en-us/windows/win32/menurc/resource-types +const RT_MANIFEST_TYPE = 24; + +export async function resedit(exePath: string, options: ExeMetadata) { + const resedit = await loadResEdit(); + + const exeData = await fs.readFile(exePath); + const exe = resedit.NtExecutable.from(exeData); + const res = resedit.NtExecutableResource.from(exe); + + if (options.iconPath) { + // Icon Info + const existingIconGroups = resedit.Resource.IconGroupEntry.fromEntries(res.entries); + if (existingIconGroups.length !== 1) { + throw new Error('Failed to parse win32 executable resources, failed to locate existing icon group'); + } + const iconFile = resedit.Data.IconFile.from(await fs.readFile(options.iconPath)); + resedit.Resource.IconGroupEntry.replaceIconsForResource( + res.entries, + existingIconGroups[0].id, + existingIconGroups[0].lang, + iconFile.icons.map((item) => item.data) + ); + } + + // Manifest + if (options.win32Metadata?.['application-manifest'] || options.win32Metadata?.['requested-execution-level']) { + if (options.win32Metadata?.['application-manifest'] && options.win32Metadata?.['requested-execution-level']) { + throw new Error('application-manifest and requested-execution-level are mutually exclusive, only provide one'); + } + + const manifests = res.entries.filter(e => e.type === RT_MANIFEST_TYPE); + if (manifests.length !== 1) { + throw new Error('Failed to parse win32 executable resources, failed to locate existing manifest'); + } + const manifestEntry = manifests[0]; + if (options.win32Metadata?.['application-manifest']) { + manifestEntry.bin = (await fs.readFile(options.win32Metadata?.['application-manifest'])).buffer; + } else if (options.win32Metadata?.['requested-execution-level']) { + // This implementation matches what rcedit used to do, in theory we can be Smarter + // and use an actual XML parser, but for now let's match the old impl + const currentManifestContent = Buffer.from(manifestEntry.bin).toString('utf-8'); + const newContent = currentManifestContent.replace( + /()/g, + `$1${options.win32Metadata?.['requested-execution-level']}$2` + ); + manifestEntry.bin = Buffer.from(newContent, 'utf-8'); + } + } + + // Version Info + const versionInfo = resedit.Resource.VersionInfo.fromEntries(res.entries); + if (versionInfo.length !== 1) { + throw new Error('Failed to parse win32 executable resources, failed to locate existing version info'); + } + if (options.fileVersion) versionInfo[0].setFileVersion(...parseVersionString(options.fileVersion)); + if (options.productVersion) versionInfo[0].setProductVersion(...parseVersionString(options.productVersion)); + const languageInfo = versionInfo[0].getAllLanguagesForStringValues(); + if (languageInfo.length !== 1) { + throw new Error('Failed to parse win32 executable resources, failed to locate existing language info'); + } + // Empty strings retain original value + const newStrings: Record = { + CompanyName: options.win32Metadata?.CompanyName || '', + FileDescription: options.win32Metadata?.FileDescription || '', + FileVersion: options.fileVersion || '', + InternalName: options.win32Metadata?.InternalName || '', + LegalCopyright: options.legalCopyright || '', + OriginalFilename: options.win32Metadata?.OriginalFilename || '', + ProductName: options.productName || '', + ProductVersion: options.productVersion || '', + }; + for (const key of Object.keys(newStrings)) { + if (!newStrings[key]) delete newStrings[key]; + } + versionInfo[0].setStringValues(languageInfo[0], newStrings); + + // Output version info + versionInfo[0].outputToResourceEntries(res.entries); + + res.outputResource(exe); + + await fs.writeFile(exePath, Buffer.from(exe.generate())); +} diff --git a/src/types.ts b/src/types.ts index 06aea645..1007b1a4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -151,8 +151,6 @@ export interface WindowsSignOptions extends Omit { const opts = { ...win32Opts, ...extraOpts } - const rcOpts = generateRceditOptionsSansIcon(opts) + const rcOpts = generateReseditOptionsSansIcon(opts) metadataProperties = [].concat(metadataProperties) expectedValues = [].concat(expectedValues) @@ -31,13 +33,7 @@ function generateVersionStringTest (metadataProperties, extraOpts, expectedValue metadataProperties.forEach((property, i) => { const value = expectedValues[i] const msg = assertionMsgs[i] - if (property === 'version-string') { - for (const subkey in value) { - t.is(rcOpts[property][subkey], value[subkey], `${msg} (${subkey})`) - } - } else { - t.is(rcOpts[property], value, msg) - } + t.is(_.get(rcOpts, property), value, msg) }) } } @@ -50,7 +46,7 @@ function setFileVersionTest (buildVersion) { } return generateVersionStringTest( - ['product-version', 'file-version'], + ['productVersion', 'fileVersion'], opts, [appVersion, buildVersion], ['Product version should match app version', @@ -60,7 +56,7 @@ function setFileVersionTest (buildVersion) { function setProductVersionTest (appVersion) { return generateVersionStringTest( - ['product-version', 'file-version'], + ['productVersion', 'fileVersion'], { appVersion: appVersion }, [appVersion, appVersion], ['Product version should match app version', @@ -73,7 +69,7 @@ function setCopyrightTest (appCopyright) { appCopyright: appCopyright } - return generateVersionStringTest('version-string', opts, { LegalCopyright: appCopyright }, 'Legal copyright should match app copyright') + return generateVersionStringTest(['legalCopyright'], opts, [appCopyright], 'Legal copyright should match app copyright') } function setCopyrightAndCompanyNameTest (appCopyright, companyName) { @@ -85,9 +81,9 @@ function setCopyrightAndCompanyNameTest (appCopyright, companyName) { } return generateVersionStringTest( - 'version-string', + ['legalCopyright', 'win32Metadata.CompanyName'], opts, - { LegalCopyright: appCopyright, CompanyName: companyName }, + [appCopyright, companyName], 'Legal copyright should match app copyright and Company name should match win32metadata value' ) } @@ -100,9 +96,9 @@ function setRequestedExecutionLevelTest (requestedExecutionLevel) { } return generateVersionStringTest( - 'requested-execution-level', + ['win32Metadata.requested-execution-level'], opts, - requestedExecutionLevel, + [requestedExecutionLevel], 'requested-execution-level in win32metadata should match rcOpts value' ) } @@ -115,9 +111,9 @@ function setApplicationManifestTest (applicationManifest) { } return generateVersionStringTest( - 'application-manifest', + ['win32Metadata.application-manifest'], opts, - applicationManifest, + [applicationManifest], 'application-manifest in win32metadata should match rcOpts value' ) } @@ -129,54 +125,20 @@ function setCompanyNameTest (companyName) { } } - return generateVersionStringTest('version-string', + return generateVersionStringTest(['win32Metadata.CompanyName'], opts, - { CompanyName: companyName }, + [companyName], 'Company name should match win32metadata value') } -test('better error message when wine is not found', t => { - const err = new WrapperError('wine-nonexistent') - - t.notRegex(err.message, /win32metadata/) - const augmentedError = updateWineMissingException(err) - t.regex(augmentedError.message, /win32metadata/) -}) - -test('error message unchanged when error not about wine missing', t => { - const notWrapperError = Error('Not a wrapper error') - - const returnedError = updateWineMissingException(notWrapperError) - t.is(returnedError.message, 'Not a wrapper error') -}) - -// Wine-using platforms only; macOS exhibits a strange behavior in CI, -// so we're disabling it there as well. -if (process.platform === 'linux') { - test.serial('win32 integration: catches a missing wine executable', util.packagerTest(async (t, opts) => { - process.env.WINE_BINARY = 'wine-nonexistent' - try { - await t.throwsAsync(() => packager({ - ...opts, - ...win32Opts - }), { - instanceOf: WrapperError, - message: /wine-nonexistent.*win32metadata/ms - }) - } finally { - delete process.env.WINE_BINARY - } - })) -} - test('win32metadata defaults', t => { const opts = { name: 'Win32 App' } - const rcOpts = generateRceditOptionsSansIcon(opts) + const rcOpts = generateReseditOptionsSansIcon(opts) - t.is(rcOpts['version-string'].FileDescription, opts.name, 'default FileDescription') - t.is(rcOpts['version-string'].InternalName, opts.name, 'default InternalName') - t.is(rcOpts['version-string'].OriginalFilename, 'Win32 App.exe', 'default OriginalFilename') - t.is(rcOpts['version-string'].ProductName, opts.name, 'default ProductName') + t.is(rcOpts.win32Metadata.FileDescription, opts.name, 'default FileDescription') + t.is(rcOpts.win32Metadata.InternalName, opts.name, 'default InternalName') + t.is(rcOpts.win32Metadata.OriginalFilename, 'Win32 App.exe', 'default OriginalFilename') + t.is(rcOpts.productName, opts.name, 'default ProductName') }) function win32Test (extraOpts, executableBasename, executableMessage) { @@ -185,34 +147,70 @@ function win32Test (extraOpts, executableBasename, executableMessage) { const paths = await packager(opts) t.is(1, paths.length, '1 bundle created') - await util.assertPathExists(t, path.join(paths[0], `${executableBasename}.exe`), executableMessage) + const exePath = path.join(paths[0], `${executableBasename}.exe`) + await util.assertPathExists(t, exePath, executableMessage) + return exePath }) } -if (!(process.env.CI && process.platform === 'darwin')) { - test.serial('win32: executable name is based on sanitized app name', win32Test( - { name: '@username/package-name' }, - '@username-package-name', - 'The sanitized EXE filename should exist' - )) - - test.serial('win32: executable name uses executableName when available', win32Test( - { name: 'PackageName', executableName: 'my-package' }, - 'my-package', - 'the executableName-based filename should exist' - )) - - test.serial('win32: set icon', win32Test( - { executableName: 'iconTest', arch: 'ia32', icon: path.join(__dirname, 'fixtures', 'monochrome') }, - 'iconTest', +test.serial('win32: executable name is based on sanitized app name', win32Test( + { name: '@username/package-name' }, + '@username-package-name', + 'The sanitized EXE filename should exist' +)) + +test.serial('win32: executable name uses executableName when available', win32Test( + { name: 'PackageName', executableName: 'my-package' }, + 'my-package', + 'the executableName-based filename should exist' +)) + +test.serial('win32: set icon', win32Test( + { executableName: 'iconTest', arch: 'ia32', icon: path.join(__dirname, 'fixtures', 'monochrome') }, + 'iconTest', + 'the Electron executable should exist' +)) + +test.serial('win32: version info is set correctly in final exe', async t => { + const exePath = await win32Test( + { executableName: 'versionInfoTest', arch: 'x64', appVersion: '1.2.3', buildVersion: '4.5.6' }, + 'versionInfoTest', 'the Electron executable should exist' - )) - - test('win32: build version sets FileVersion', setFileVersionTest('2.3.4.5')) - test('win32: app version sets ProductVersion', setProductVersionTest('5.4.3.2')) - test('win32: app copyright sets LegalCopyright', setCopyrightTest('Copyright Bar')) - test('win32: set LegalCopyright and CompanyName', setCopyrightAndCompanyNameTest('Copyright Bar', 'MyCompany LLC')) - test('win32: set CompanyName', setCompanyNameTest('MyCompany LLC')) - test('win32: set requested-execution-level', setRequestedExecutionLevelTest('asInvoker')) - test('win32: set application-manifest', setApplicationManifestTest('/path/to/manifest.xml')) -} + )(t) + + const resedit = await loadResedit() + const exe = resedit.NtExecutable.from(await fs.readFile(exePath)) + const res = resedit.NtExecutableResource.from(exe) + + const versionInfo = resedit.Resource.VersionInfo.fromEntries(res.entries) + t.is(versionInfo.length, 1, 'should only have one version info resource') + const version = versionInfo[0] + const langs = version.getAllLanguagesForStringValues() + t.is(langs.length, 1, 'should only have one lang') + t.is(version.getStringValues(langs[0]).FileVersion, '4.5.6', 'file version should match build version') + t.is(version.getStringValues(langs[0]).ProductVersion, '1.2.3', 'product version should match app version') +}) + +test.serial('win32: requested execution level is set correctly in final exe', async t => { + const exePath = await win32Test( + { executableName: 'versionInfoTest', arch: 'x64', win32metadata: { 'requested-execution-level': 'requireAdministrator' } }, + 'versionInfoTest', + 'the Electron executable should exist' + )(t) + + const resedit = await loadResedit() + const exe = resedit.NtExecutable.from(await fs.readFile(exePath)) + const res = resedit.NtExecutableResource.from(exe) + + const manifest = res.entries.find(e => e.type === 24) + const manifestString = Buffer.from(manifest.bin).toString('utf-8') + t.is(manifestString.includes('requireAdministrator'), true, 'should have the new level in the manifest') +}) + +test('win32: build version sets FileVersion', setFileVersionTest('2.3.4.5')) +test('win32: app version sets ProductVersion', setProductVersionTest('5.4.3.2')) +test('win32: app copyright sets LegalCopyright', setCopyrightTest('Copyright Bar')) +test('win32: set LegalCopyright and CompanyName', setCopyrightAndCompanyNameTest('Copyright Bar', 'MyCompany LLC')) +test('win32: set CompanyName', setCompanyNameTest('MyCompany LLC')) +test('win32: set requested-execution-level', setRequestedExecutionLevelTest('asInvoker')) +test('win32: set application-manifest', setApplicationManifestTest('/path/to/manifest.xml')) diff --git a/yarn.lock b/yarn.lock index 3781880b..3153360a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -467,13 +467,6 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@malept/cross-spawn-promise@^1.1.0": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz#504af200af6b98e198bce768bc1730c6936ae01d" - integrity sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ== - dependencies: - cross-spawn "^7.0.1" - "@malept/cross-spawn-promise@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz#d0772de1aa680a0bfb9ba2f32b4c828c7857cb9d" @@ -1507,15 +1500,6 @@ cross-dirname@^0.1.0: resolved "https://registry.yarnpkg.com/cross-dirname/-/cross-dirname-0.1.0.tgz#b899599f30a5389f59e78c150e19f957ad16a37c" integrity sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q== -cross-spawn-windows-exe@^1.1.0, cross-spawn-windows-exe@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/cross-spawn-windows-exe/-/cross-spawn-windows-exe-1.2.0.tgz#46253b0f497676e766faf4a7061004618b5ac5ec" - integrity sha512-mkLtJJcYbDCxEG7Js6eUnUNndWjyUZwJ3H7bErmmtOYU/Zb99DyUkpamuIZE0b3bhmJyZ7D90uS6f+CGxRRjOw== - dependencies: - "@malept/cross-spawn-promise" "^1.1.0" - is-wsl "^2.2.0" - which "^2.0.2" - cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -2758,11 +2742,6 @@ is-date-object@^1.0.1: dependencies: has-tostringtag "^1.0.0" -is-docker@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" - integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== - is-error@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/is-error/-/is-error-2.2.2.tgz#c10ade187b3c93510c5470a5567833ee25649843" @@ -2908,13 +2887,6 @@ is-windows@^1.0.2: resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== -is-wsl@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" - integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== - dependencies: - is-docker "^2.0.0" - is-yarn-global@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" @@ -3208,7 +3180,7 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@^4.13.1, lodash@^4.17.15, lodash@^4.17.20: +lodash@^4.13.1, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -3775,6 +3747,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pe-library@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/pe-library/-/pe-library-1.0.0.tgz#360934ccdc25f19ac24d61c9a347caf23a9dc27a" + integrity sha512-yZ+4d3YHKUjO0BX03oXFfHRKLdYKDO2HmCt1RcApPxme/P5ASPbbKnuQkzFrmT482wi2kfO+sPgqasrz5QeU1w== + pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" @@ -3934,13 +3911,6 @@ rc@1.2.8, rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" -rcedit@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/rcedit/-/rcedit-4.0.1.tgz#892ac47a19204a380f49e00ea38ce070443343c2" - integrity sha512-bZdaQi34krFWhrDn+O53ccBDw0MkAT2Vhu75SqhtvhQu4OPyFM4RoVheyYiVQYdjhUi6EJMVWQ0tR6bCIYVkUg== - dependencies: - cross-spawn-windows-exe "^1.1.0" - read-pkg-up@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" @@ -4052,6 +4022,13 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== +resedit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resedit/-/resedit-2.0.0.tgz#cd615f294c38e4b806cf031ede128d5e0c17d1d7" + integrity sha512-vrrJCabKxAW4MT1QivtAAb0poGp8KT2qhnSzfN9tFIxb2rQu1hRHNn1VgGSZR7nmxGaW5Yz0YeW1bjgvRfNoKA== + dependencies: + pe-library "^1.0.0" + resolve-alpn@^1.0.0: version "1.2.1" resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" @@ -4841,7 +4818,7 @@ which-typed-array@^1.1.13: gopd "^1.0.1" has-tostringtag "^1.0.0" -which@^2.0.1, which@^2.0.2: +which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==