Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion src/lib/getAllPackages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type PnpmWorkspaces =
| string[]
| { packages: string[]; catalog?: Index<VersionSpec>; catalogs?: Index<Index<VersionSpec>> }

type YarnConfig = { catalog?: Index<VersionSpec>; catalogs?: Index<Index<VersionSpec>> }

const globOptions: GlobOptions = {
ignore: ['**/node_modules/**'],
}
Expand All @@ -32,6 +34,18 @@ const readPnpmWorkspaces = async (pkgPath: string): Promise<PnpmWorkspaces | nul
return yaml.load(pnpmWorkspaceFile) as PnpmWorkspaces
}

/** Reads, parses, and resolves catalog information from the yarn config file at the same path as the package file. */
const readYarnConfig = async (pkgPath: string): Promise<YarnConfig | null> => {
const yarnConfigPath = path.join(path.dirname(pkgPath), 'yarnrc.yml')
let yarnConfig: string
try {
yarnConfig = await fs.readFile(yarnConfigPath, 'utf-8')
} catch {
return null
}
return yaml.load(yarnConfig) as YarnConfig
}

/** Gets catalog dependencies from both pnpm-workspace.yaml and package.json files. */
const readCatalogDependencies = async (options: Options, pkgPath: string): Promise<Index<VersionSpec> | null> => {
const catalogDependencies: Index<VersionSpec> = {}
Expand All @@ -50,6 +64,18 @@ const readCatalogDependencies = async (options: Options, pkgPath: string): Promi
}
}

if (options.packageManager === 'yarn') {
const yarnConfig = await readYarnConfig(pkgPath)
if (yarnConfig) {
if (yarnConfig.catalog) {
Object.assign(catalogDependencies, yarnConfig.catalog)
}
if (yarnConfig.catalogs) {
Object.assign(catalogDependencies, ...Object.values(yarnConfig.catalogs))
}
}
}

// Read from package.json (for Bun and modern pnpm)
const packageData: PackageFile & {
catalog?: Index<VersionSpec>
Expand Down Expand Up @@ -177,7 +203,11 @@ async function getCatalogPackageInfo(options: Options, pkgPath: string): Promise
// Determine the correct file path for catalogs. For pnpm, use pnpm-workspace.yaml.
// For Bun catalogs in package.json, use a virtual path to avoid conflicts with root package.
const catalogFilePath =
options.packageManager === 'pnpm' ? path.join(path.dirname(pkgPath), 'pnpm-workspace.yaml') : `${pkgPath}#catalog`
options.packageManager === 'pnpm'
? path.join(path.dirname(pkgPath), 'pnpm-workspace.yaml')
: options.packageManager === 'yarn'
? path.join(path.dirname(pkgPath), 'yarnrc.yml')
: `${pkgPath}#catalog`

// Create synthetic file content that matches the synthetic PackageFile
const syntheticFileContent = JSON.stringify(catalogPackageFile, null, 2)
Expand Down
6 changes: 1 addition & 5 deletions src/lib/upgradePackageData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ async function upgradePackageData(
// Handle pnpm-workspace.yaml catalog files
if (
fileName === 'pnpm-workspace.yaml' ||
fileName === 'yarnrc.yml' ||
(fileName.includes('catalog') && (fileExtension === '.yaml' || fileExtension === '.yml'))
) {
// Check if we have synthetic catalog data (JSON with only dependencies and name/version)
Expand Down Expand Up @@ -100,11 +101,6 @@ async function upgradePackageData(
}
}

// For pnpm, also expose the 'default' catalog as a top-level 'catalog' property
if (yamlData.catalogs?.default) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't make really sense to me and would create an catalog entry which wasn't defined before (see also workspace.test.ts)

Copy link
Owner

@raineorshine raineorshine Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure this was wrong? I'm hesitant to remove anything from the existing catalog implementation, as this would be a breaking change.

i.e. is this a patch change or major change, and how many people are relying on this behavior currently.

@jakeboone02 Can you weigh in on the purpose of this block and the implications of removing it?

Copy link
Contributor Author

@MKruschke MKruschke Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @raineorshine, Hi @jakeboone02 ,
yes I'm pretty sure that this is the wrong behaviour and needs to be removed. Would even classify it as bug and not as breaking change.

Details:

For everyone that explicit is using a named catalog with the name default (e.g. catalogs.default it will also break the usage of pnpm (see provide screenshot) because it will automatically introduce the default catalog catalog (in the current ncu implementation called singular catalog) after running ncu. I tested it without ncu just added a named catalog default and run install with v10.13.1 as well as v10.26.2 of pnpm.

image

yamlData.catalog = yamlData.catalogs.default
}

return JSON.stringify(yamlData, null, 2)
}

Expand Down
123 changes: 122 additions & 1 deletion test/workspaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,6 @@ catalogs:
output.should.deep.equal({
'pnpm-workspace.yaml': {
packages: ['packages/**'],
catalog: { 'ncu-test-v2': '2.0.0' },
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this shouldn't happen because catalog wasn't defined in the config before so it shouldn't be created by ncu

catalogs: { default: { 'ncu-test-v2': '2.0.0' }, test: { 'ncu-test-tag': '1.1.0' } },
},
'package.json': { dependencies: { 'ncu-test-v2': '2.0.0' } },
Expand Down Expand Up @@ -696,6 +695,128 @@ catalog:
})
})

describe('yarn', () => {
it('update yarn catalog dependencies from yarnrc.yml (named catalogs)', async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'npm-check-updates-'))
try {
const pkgDataRoot = JSON.stringify({
workspaces: ['packages/**'],
dependencies: {
'ncu-test-v2': '1.0.0',
},
})

const yarnConfig = `
catalogs:
default:
ncu-test-v2: '1.0.0'
test:
ncu-test-tag: '1.0.0'
`

// write root package file and yarnrc.yml
await fs.writeFile(path.join(tempDir, 'package.json'), pkgDataRoot, 'utf-8')
await fs.writeFile(path.join(tempDir, 'yarnrc.yml'), yarnConfig, 'utf-8')
await fs.writeFile(path.join(tempDir, 'yarn.lock'), '', 'utf-8')

// create workspace package
await fs.mkdir(path.join(tempDir, 'packages/a'), { recursive: true })
await fs.writeFile(
path.join(tempDir, 'packages/a/package.json'),
JSON.stringify({
dependencies: {
'ncu-test-tag': 'catalog:test',
},
}),
'utf-8',
)

const { stdout, stderr } = await spawn(
'node',
[bin, '--jsonAll', '--workspaces'],
{ rejectOnError: false },
{ cwd: tempDir },
)

// Assert no errors and valid output
stderr.should.be.empty
stdout.should.not.be.empty

const output = JSON.parse(stdout)

// Should include catalog updates
output.should.deep.equal({
'yarnrc.yml': {
catalogs: { default: { 'ncu-test-v2': '2.0.0' }, test: { 'ncu-test-tag': '1.1.0' } },
},
'package.json': { workspaces: ['packages/**'], dependencies: { 'ncu-test-v2': '2.0.0' } },
'packages/a/package.json': { dependencies: { 'ncu-test-tag': 'catalog:test' } },
})
} finally {
await fs.rm(tempDir, { recursive: true, force: true })
}
})

it('update yarn catalog dependencies from yarnrc.yml (singular catalog)', async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'npm-check-updates-'))
try {
const pkgDataRoot = JSON.stringify({
workspaces: ['packages/**'],
dependencies: {
'ncu-test-v2': '1.0.0',
},
})

const yarnConfig = `
catalog:
ncu-test-v2: '1.0.0'
ncu-test-tag: '1.0.0'
`

// write root package file and pnpm-workspace.yaml
await fs.writeFile(path.join(tempDir, 'package.json'), pkgDataRoot, 'utf-8')
await fs.writeFile(path.join(tempDir, 'yarnrc.yml'), yarnConfig, 'utf-8')
await fs.writeFile(path.join(tempDir, 'yarn.lock'), '', 'utf-8')

// create workspace package
await fs.mkdir(path.join(tempDir, 'packages/a'), { recursive: true })
await fs.writeFile(
path.join(tempDir, 'packages/a/package.json'),
JSON.stringify({
dependencies: {
'ncu-test-tag': 'catalog:',
},
}),
'utf-8',
)

const { stdout, stderr } = await spawn(
'node',
[bin, '--jsonAll', '--workspaces'],
{ rejectOnError: false },
{ cwd: tempDir },
)

// Assert no errors and valid output
stderr.should.be.empty
stdout.should.not.be.empty

const output = JSON.parse(stdout)

// Should include catalog updates
output.should.deep.equal({
'yarnrc.yml': {
catalog: { 'ncu-test-v2': '2.0.0', 'ncu-test-tag': '1.1.0' },
},
'package.json': { workspaces: ['packages/**'], dependencies: { 'ncu-test-v2': '2.0.0' } },
'packages/a/package.json': { dependencies: { 'ncu-test-tag': 'catalog:' } },
})
} finally {
await fs.rm(tempDir, { recursive: true, force: true })
}
})
})

describe('bun', () => {
it('update bun catalog dependencies from package.json (top-level)', async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'npm-check-updates-'))
Expand Down
Loading