diff --git a/packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-berry-project/.gitattributes b/packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-berry-project/.gitattributes new file mode 100644 index 000000000..af3ad1281 --- /dev/null +++ b/packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-berry-project/.gitattributes @@ -0,0 +1,4 @@ +/.yarn/** linguist-vendored +/.yarn/releases/* binary +/.yarn/plugins/**/* binary +/.pnp.* binary linguist-generated diff --git a/packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-berry-project/.gitignore b/packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-berry-project/.gitignore new file mode 100644 index 000000000..870eb6a50 --- /dev/null +++ b/packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-berry-project/.gitignore @@ -0,0 +1,13 @@ +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Swap the comments on the following lines if you wish to use zero-installs +# In that case, don't forget to run `yarn config set enableGlobalCache false`! +# Documentation here: https://yarnpkg.com/features/caching#zero-installs + +#!.yarn/cache +.pnp.* diff --git a/packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-berry-project/package.json b/packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-berry-project/package.json new file mode 100644 index 000000000..00fd19335 --- /dev/null +++ b/packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-berry-project/package.json @@ -0,0 +1,10 @@ +{ + "name": "yarn-berry-ls", + "packageManager": "yarn@4.4.0", + "dependencies": { + "mime-types": "2.1.35" + }, + "devDependencies": { + "flatten": "1.0.3" + } +} diff --git a/packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-berry-project/yarn.lock b/packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-berry-project/yarn.lock new file mode 100644 index 000000000..17825934b --- /dev/null +++ b/packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-berry-project/yarn.lock @@ -0,0 +1,38 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"flatten@npm:1.0.3": + version: 1.0.3 + resolution: "flatten@npm:1.0.3" + checksum: 10c0/9f9b1f3dcd05be057bb83ec27f2513da5306e7bfc0cf8bd839ab423eb1b0f99683a25c97b48fafd5959819159659ce9f1397623a46f89a8577ba095fcf5fb753 + languageName: node + linkType: hard + +"mime-db@npm:1.52.0": + version: 1.52.0 + resolution: "mime-db@npm:1.52.0" + checksum: 10c0/0557a01deebf45ac5f5777fe7740b2a5c309c6d62d40ceab4e23da9f821899ce7a900b7ac8157d4548ddbb7beffe9abc621250e6d182b0397ec7f10c7b91a5aa + languageName: node + linkType: hard + +"mime-types@npm:2.1.35": + version: 2.1.35 + resolution: "mime-types@npm:2.1.35" + dependencies: + mime-db: "npm:1.52.0" + checksum: 10c0/82fb07ec56d8ff1fc999a84f2f217aa46cb6ed1033fefaabd5785b9a974ed225c90dc72fff460259e66b95b73648596dbcc50d51ed69cdf464af2d237d3149b2 + languageName: node + linkType: hard + +"yarn-berry-ls@workspace:.": + version: 0.0.0-use.local + resolution: "yarn-berry-ls@workspace:." + dependencies: + flatten: "npm:1.0.3" + mime-types: "npm:2.1.35" + languageName: unknown + linkType: soft diff --git a/packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-project/package.json b/packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-classic-project/package.json similarity index 83% rename from packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-project/package.json rename to packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-classic-project/package.json index f19e87458..8f6cca076 100644 --- a/packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-project/package.json +++ b/packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-classic-project/package.json @@ -1,5 +1,5 @@ { - "name": "yarn-ls", + "name": "yarn-classic-ls", "packageManager": "yarn@1.22.22", "dependencies": { "mime-types": "2.1.35" diff --git a/packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-project/yarn.lock b/packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-classic-project/yarn.lock similarity index 100% rename from packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-project/yarn.lock rename to packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-classic-project/yarn.lock diff --git a/packages/wxt/src/core/package-managers/__tests__/yarn.test.ts b/packages/wxt/src/core/package-managers/__tests__/yarn.test.ts index 40415b7df..67aa24af4 100644 --- a/packages/wxt/src/core/package-managers/__tests__/yarn.test.ts +++ b/packages/wxt/src/core/package-managers/__tests__/yarn.test.ts @@ -3,8 +3,8 @@ import path from 'node:path'; import { yarn } from '../yarn'; describe('Yarn Package Management Utils', () => { - describe('listDependencies', () => { - const cwd = path.resolve(__dirname, 'fixtures/simple-yarn-project'); + describe('listDependencies (Yarn 1 / Classic)', () => { + const cwd = path.resolve(__dirname, 'fixtures/simple-yarn-classic-project'); it('should list direct dependencies', async () => { const actual = await yarn.listDependencies({ cwd }); @@ -24,4 +24,25 @@ describe('Yarn Package Management Utils', () => { ]); }); }); + + describe('listDependencies (Yarn 2+ / Berry)', () => { + const cwd = path.resolve(__dirname, 'fixtures/simple-yarn-berry-project'); + + it('should list direct dependencies', async () => { + const actual = await yarn.listDependencies({ cwd }); + expect(actual).toEqual([ + { name: 'mime-types', version: 'npm:2.1.35' }, + { name: 'flatten', version: 'npm:1.0.3' }, + ]); + }); + + it('should list all dependencies', async () => { + const actual = await yarn.listDependencies({ cwd, all: true }); + expect(actual).toEqual([ + { name: 'mime-types', version: 'npm:2.1.35' }, + { name: 'mime-db', version: 'npm:1.52.0' }, + { name: 'flatten', version: 'npm:1.0.3' }, + ]); + }); + }); }); diff --git a/packages/wxt/src/core/package-managers/yarn.ts b/packages/wxt/src/core/package-managers/yarn.ts index c313ca8d6..8e64bcc46 100644 --- a/packages/wxt/src/core/package-managers/yarn.ts +++ b/packages/wxt/src/core/package-managers/yarn.ts @@ -1,8 +1,12 @@ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { pathExists } from 'fs-extra'; import { Dependency } from '../../types'; import { WxtPackageManagerImpl } from './types'; import { dedupeDependencies, npm } from './npm'; -export const yarn: WxtPackageManagerImpl = { +// Yarn 1 (Classic) package manager implementation +export const yarnClassic: WxtPackageManagerImpl = { overridesKey: 'resolutions', downloadDependency(...args) { return npm.downloadDependency(...args); @@ -39,6 +43,88 @@ export const yarn: WxtPackageManagerImpl = { }, }; +// Returns the absolute path of the root directory of a Yarn Berry mono-repository +const getMonorepoRootDir = async () => { + let monorepoRootDir = dirname(fileURLToPath(import.meta.url)); + while (!(await pathExists(resolve(monorepoRootDir, 'yarn.lock'))) && monorepoRootDir !== '/') { + monorepoRootDir = dirname(monorepoRootDir); + } + return monorepoRootDir; +} + +// yarn 2+ (Berry) package manager implementation +export const yarnBerry: WxtPackageManagerImpl = { + overridesKey: 'resolutions', + async downloadDependency(id: string, downloadDir: string) { + const { execa } = await import('execa'); + if (!id.includes('@workspace:')) { + return npm.downloadDependency(id.replace('@npm:', '@'), downloadDir); + } + const monorepoRootDir = await getMonorepoRootDir(); + const [dependencyName, dependencyDirRelativeToMonorepoRootDir] = id.split('@workspace:'); + const dependencyDir = resolve(monorepoRootDir, dependencyDirRelativeToMonorepoRootDir); + const packedFilename = `${dependencyName.replace('@', '').replace('/', '-')}.tgz`; + const archivePath = resolve(downloadDir, packedFilename); + await execa('yarn', ['pack', '--out', archivePath], { + cwd: dependencyDir + }); + return archivePath; + }, + async listDependencies(options) { + const monorepoRootDir = await getMonorepoRootDir(); + let currentWorkspace = '.'; + if (monorepoRootDir !== '/' && options?.cwd?.startsWith(monorepoRootDir)) { + currentWorkspace = options.cwd.substring(monorepoRootDir.length); + } + + const args = ['info', '--name-only', '--json']; + if (options?.all) { + args.push('--all'); + args.push('--recursive'); + } + const { execa } = await import('execa'); + const res = await execa('yarn', args, { cwd: options?.cwd }); + const lines = res.stdout.split('\n').map((line) => JSON.parse(line)); + + const dependencies: Dependency[] = []; + + while (lines.length > 0) { + const line = lines.pop(); + // example output formats + // - "foo@npm:0.0.1" + // - "@acme/foo@npm:1.2.3" + // - "@acme/bar@workspace:packages/bar" + // - "typescript@patch:typescript@npm%3A5.6.2#optional!builtin::version=5.6.2&hash=8c6c40" + const name = line.substring(0, line.substring(1).indexOf('@') + 1); + const version = line.substring(name.length + 1); + const isCurrentPackage = version === `workspace:${currentWorkspace}`; + if (name === '' || version === '' || isCurrentPackage) { + continue; + } + dependencies.push({ name, version }); + } + + return dedupeDependencies(dependencies); + }, +}; + +// Yarn 1 (Classic) and Yarn 2+ (Berry) have different CLI and output formats +export const yarn: WxtPackageManagerImpl = { + overridesKey: 'resolutions', + async downloadDependency(id: string, downloadDir: string) { + const { execa } = await import('execa'); + const execRes = await execa('yarn', ['--version']); + const _yarn = execRes.stdout.startsWith('1.') ? yarnClassic : yarnBerry; + return _yarn.downloadDependency(id, downloadDir); + }, + async listDependencies(options) { + const { execa } = await import('execa'); + const execRes = await execa('yarn', ['--version'], { cwd: options?.cwd }); + const _yarn = execRes.stdout.startsWith('1.') ? yarnClassic : yarnBerry; + return _yarn.listDependencies(options); + } +}; + type JsonLine = | { type: unknown; data: unknown } | { type: 'tree'; data: JsonLineTree }; diff --git a/packages/wxt/src/core/zip.ts b/packages/wxt/src/core/zip.ts index c44f806b2..0ab3fe77a 100644 --- a/packages/wxt/src/core/zip.ts +++ b/packages/wxt/src/core/zip.ts @@ -170,6 +170,16 @@ async function downloadPrivatePackages() { ); for (const pkg of downloadPackages) { + const protocol = pkg.version.split(':')[0]; + const isDownloadable = !['file', 'patch'].includes(protocol); + + if (!isDownloadable) { + wxt.logger.warn( + `Skipping package download: ${pkg.name}@${pkg.version}`, + ); + continue; + } + wxt.logger.info(`Downloading package: ${pkg.name}@${pkg.version}`); const id = `${pkg.name}@${pkg.version}`; const tgzPath = await wxt.pm.downloadDependency( @@ -177,7 +187,10 @@ async function downloadPrivatePackages() { wxt.config.zip.downloadedPackagesDir, ); files.push(tgzPath); - overrides[id] = tgzPath; + + // removes version strings that may cause issues with Yarn Berry + const overrideKey = id.replace(/@(npm|workspace):.*$/, ''); + overrides[overrideKey] = tgzPath; } } diff --git a/packages/wxt/src/types.ts b/packages/wxt/src/types.ts index d556143e9..0c5f00cd9 100644 --- a/packages/wxt/src/types.ts +++ b/packages/wxt/src/types.ts @@ -1438,7 +1438,8 @@ export interface WxtPackageManager extends Nypm.PackageManager { /** * Run `npm ls`, `pnpm ls`, or `bun pm ls`, or `yarn list` and return the results. * - * WARNING: Yarn always returns all dependencies + * WARNING: Yarn Classic always returns all dependencies + * WARNING: Yarn Berry prefixes dependency versions with a protocol name (such as `npm:` or `workspace:`) */ listDependencies: (options?: { cwd?: string;