From 3b8d2cfadb70ed24168b10dc640284fd11113b7c Mon Sep 17 00:00:00 2001 From: James Barr Date: Sat, 16 May 2026 20:02:32 +0100 Subject: [PATCH 1/4] test: add tests demonstrating limitations of the toml regex edit --- test/toml.test.js | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/test/toml.test.js b/test/toml.test.js index 9f06365..974430d 100644 --- a/test/toml.test.js +++ b/test/toml.test.js @@ -11,7 +11,11 @@ import { readFile } from './globals/file-utils.js'; mock({ './foo.toml': `[tool.test]${EOL}version = "${CURRENT_VERSION}"${EOL}`, './cargo.toml': `[workspace]${EOL}${EOL}[package]${EOL}name = "hello_world"${EOL}version = "${CURRENT_VERSION}"${EOL}authors = [ "Alice ", "Bob " ]${EOL}${EOL}[dependencies]${EOL}time = "0.1.12"${EOL}`, - './pyproject.toml': `[project]${EOL}name = "foo"${EOL}version = "${CURRENT_VERSION}"${EOL}# these are authors${EOL}authors = [{ name = "Alice", email = "a@example.com" }]${EOL}` + './pyproject.toml': `[project]${EOL}name = "foo"${EOL}version = "${CURRENT_VERSION}"${EOL}# these are authors${EOL}authors = [{ name = "Alice", email = "a@example.com" }]${EOL}`, + // missing project version but has a tool using version key and semver + './missing_path_version.toml': `[project]${EOL}name = "foo"${EOL}[tool.commitizen]${EOL}version = "${CURRENT_VERSION}"${EOL}`, + // project version is already at NEW_VERSION and has a dependency with a version containing the CURRENT_VERSION + './at_current_version.toml': `[project]${EOL}name = "foo"${EOL}version = "${NEW_VERSION}"${EOL}dependencies = [${EOL} "django = 1${CURRENT_VERSION}"${EOL}]${EOL}`, }); describe('toml file', { concurrency: true }, () => { @@ -100,4 +104,34 @@ describe('toml file', { concurrency: true }, () => { `[project]${EOL}name = "foo"${EOL}version = "${NEW_VERSION}"${EOL}# these are authors${EOL}authors = [{ name = "Alice", email = "a@example.com" }]${EOL}` ); }); + + it('should add version at path', async () => { + const options = { + [NAMESPACE]: { + out: { file: './missing_path_version.toml', path: 'project.version' } + } + }; + const plugin = await factory(Bumper, { NAMESPACE, options }); + await runTasks(plugin); + + assert.equal( + readFile('./missing_path_version.toml'), + `[project]${EOL}name = "foo"${EOL}version = "${NEW_VERSION}"${EOL}[tool.commitizen]${EOL}version = "${CURRENT_VERSION}"${EOL}` + ); + }) + + it('should noop when the target version is already at the new version', async () => { + const options = { + [NAMESPACE]: { + out: { file: './at_current_version.toml', path: 'project.version' } + } + }; + const plugin = await factory(Bumper, { NAMESPACE, options }); + await runTasks(plugin); + + assert.equal( + readFile('./at_current_version.toml'), + `[project]${EOL}name = "foo"${EOL}version = "${NEW_VERSION}"${EOL}dependencies = [${EOL} "django = 1${CURRENT_VERSION}"${EOL}]${EOL}` + ); + }) }); From e7e943dd408705a0f385a7700b1e2c6d54b3483c Mon Sep 17 00:00:00 2001 From: James Barr Date: Sat, 16 May 2026 22:34:46 +0100 Subject: [PATCH 2/4] fix(toml): adhere to target path when patching toml formatted files Introduces `@decimalturn/toml-patch` to update TOML versions without destroying original formatting or stripping inline comments. While this package has a smaller community footprint than alternative options (like `@shopify/toml-patch`), it offers a stable semver release. --- index.js | 12 +++--------- package.json | 1 + 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index 33e026d..20ec89e 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,7 @@ import { castArray, get, set } from 'lodash-es'; import detectIndent from 'detect-indent'; import yaml from 'js-yaml'; import toml from '@iarna/toml'; +import { patch } from '@decimalturn/toml-patch'; import ini from 'ini'; import semver from 'semver'; import { Plugin } from 'release-it'; @@ -182,15 +183,8 @@ class Bumper extends Plugin { case 'yaml': return writeFileSync(file, yaml.dump(parsed, { indent: indent.length })); case 'toml': - var tomlContent = data; - - castArray(path).forEach(path => { - const latestPath = path.split('.').at(-1); - const versionMatch = new RegExp(`${latestPath}[\\W\\w]+?(${latestVersion.replaceAll('.', '\\.')})` || ''); - tomlContent = tomlContent.replace(versionMatch, (match, group1) => { - return match.replace(group1, versionPrefix + version); - }); - }); + let tomlContent = data + tomlContent = patch(tomlContent, parsed) return writeFileSync(file, tomlContent.replace(/(\r?\n)/g, newline)); case 'ini': diff --git a/package.json b/package.json index 4d08fb0..5dbdfa1 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ }, "author": "Lars Kappert ", "dependencies": { + "@decimalturn/toml-patch": "^1.3.0", "@iarna/toml": "^3.0.0", "cheerio": "^1.0.0", "detect-indent": "7.0.1", From 392a19dcf27d9e344a2f9ad9278a03bb79851b87 Mon Sep 17 00:00:00 2001 From: James Barr Date: Sat, 16 May 2026 23:26:36 +0100 Subject: [PATCH 3/4] test: refactor toml tests to align more with json tests --- test/toml.test.js | 85 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 58 insertions(+), 27 deletions(-) diff --git a/test/toml.test.js b/test/toml.test.js index 974430d..9a81cd6 100644 --- a/test/toml.test.js +++ b/test/toml.test.js @@ -10,12 +10,14 @@ import { readFile } from './globals/file-utils.js'; mock({ './foo.toml': `[tool.test]${EOL}version = "${CURRENT_VERSION}"${EOL}`, - './cargo.toml': `[workspace]${EOL}${EOL}[package]${EOL}name = "hello_world"${EOL}version = "${CURRENT_VERSION}"${EOL}authors = [ "Alice ", "Bob " ]${EOL}${EOL}[dependencies]${EOL}time = "0.1.12"${EOL}`, - './pyproject.toml': `[project]${EOL}name = "foo"${EOL}version = "${CURRENT_VERSION}"${EOL}# these are authors${EOL}authors = [{ name = "Alice", email = "a@example.com" }]${EOL}`, - // missing project version but has a tool using version key and semver - './missing_path_version.toml': `[project]${EOL}name = "foo"${EOL}[tool.commitizen]${EOL}version = "${CURRENT_VERSION}"${EOL}`, + // includes another section with a "version" key with semver value + './without_target_version.toml': `[tool.test]${EOL}[tool.ignored]${EOL}version = "${CURRENT_VERSION}"${EOL}`, + './with_multiple_version.toml': `[tool.test]${EOL}[project]${EOL}`, + './with_comments_and_formatting.toml': `# Lead with some comments${EOL}${EOL}[workspace]${EOL}${EOL}${EOL}[tool.test]${EOL}name = "hello_world"${EOL}version = "${CURRENT_VERSION}"${EOL}`, // project version is already at NEW_VERSION and has a dependency with a version containing the CURRENT_VERSION - './at_current_version.toml': `[project]${EOL}name = "foo"${EOL}version = "${NEW_VERSION}"${EOL}dependencies = [${EOL} "django = 1${CURRENT_VERSION}"${EOL}]${EOL}`, + './at_current_version.toml': `[tool.test]${EOL}version = "${NEW_VERSION}"${EOL}dependencies = [${EOL} "django = 1${CURRENT_VERSION}"${EOL}]${EOL}`, + './cargo.toml': `[workspace]${EOL}${EOL}[package]${EOL}name = "hello_world"${EOL}version = "${CURRENT_VERSION}"${EOL}authors = [ "Alice ", "Bob " ]${EOL}${EOL}[dependencies]${EOL}time = "0.1.12"${EOL}`, + './pyproject.toml': `[project]${EOL}name = "foo"${EOL}version = "${CURRENT_VERSION}"${EOL}authors = [{ name = "Alice", email = "a@example.com" }]${EOL}dependencies = [${EOL} "django = 1${CURRENT_VERSION}"${EOL}]${EOL}[tool.commitizen]${EOL}version = "${CURRENT_VERSION}"${EOL}`, }); describe('toml file', { concurrency: true }, () => { @@ -28,7 +30,22 @@ describe('toml file', { concurrency: true }, () => { assert.equal(version, CURRENT_VERSION); }); - it('should write', async () => { + it('should add version at path when missing', async () => { + const options = { + [NAMESPACE]: { + out: { + file: './without_target_version.toml', + type: 'text/toml', + path: 'tool.test.version' + } + } + }; + const plugin = await factory(Bumper, { NAMESPACE, options }); + await runTasks(plugin); + assert.equal(readFile('./without_target_version.toml'), `[tool.test]${EOL}version = "${NEW_VERSION}"${EOL}[tool.ignored]${EOL}version = "${CURRENT_VERSION}"${EOL}`); + }); + + it('should update version at path', async () => { const options = { [NAMESPACE]: { out: { @@ -43,6 +60,21 @@ describe('toml file', { concurrency: true }, () => { assert.equal(readFile('./foo.toml'), `[tool.test]${EOL}version = "${NEW_VERSION}"${EOL}`); }); + it('should update versions at multiple paths', async () => { + const options = { + [NAMESPACE]: { + out: { + file: './with_multiple_version.toml', + type: 'text/toml', + path: ['project.version', 'tool.test.version'] + } + } + }; + const plugin = await factory(Bumper, { NAMESPACE, options }); + await runTasks(plugin); + assert.equal(readFile('./with_multiple_version.toml'), `[tool.test]${EOL}version = "${NEW_VERSION}"${EOL}[project]${EOL}version = "${NEW_VERSION}"${EOL}`); + }); + it('should write without defining the type', async () => { const options = { [NAMESPACE]: { out: { file: './foo.toml', path: 'tool.test.version' } } @@ -79,59 +111,58 @@ describe('toml file', { concurrency: true }, () => { it('should read/write without formatting', async () => { const options = { [NAMESPACE]: { - in: { file: './cargo.toml', path: 'package.version' }, - out: { file: './cargo.toml', path: 'package.version' } + in: { file: './with_comments_and_formatting.toml', path: 'tool.test.version' }, + out: { file: './with_comments_and_formatting.toml', path: 'tool.test.version' } } }; const plugin = await factory(Bumper, { NAMESPACE, options }); await runTasks(plugin); assert.equal( - readFile('./cargo.toml'), - `[workspace]${EOL}${EOL}[package]${EOL}name = "hello_world"${EOL}version = "${NEW_VERSION}"${EOL}authors = [ "Alice ", "Bob " ]${EOL}${EOL}[dependencies]${EOL}time = "0.1.12"${EOL}` + readFile('./with_comments_and_formatting.toml'), + `# Lead with some comments${EOL}${EOL}[workspace]${EOL}${EOL}${EOL}[tool.test]${EOL}name = "hello_world"${EOL}version = "${NEW_VERSION}"${EOL}` ); }); - it('should read/write minimal changes', async () => { + it('should noop when the target version is already at the new version', async () => { const options = { [NAMESPACE]: { - out: { file: './pyproject.toml', path: 'project.version' } + out: { file: './at_current_version.toml', path: 'tool.test.version' } } }; const plugin = await factory(Bumper, { NAMESPACE, options }); await runTasks(plugin); + assert.equal( - readFile('./pyproject.toml'), - `[project]${EOL}name = "foo"${EOL}version = "${NEW_VERSION}"${EOL}# these are authors${EOL}authors = [{ name = "Alice", email = "a@example.com" }]${EOL}` + readFile('./at_current_version.toml'), + `[tool.test]${EOL}version = "${NEW_VERSION}"${EOL}dependencies = [${EOL} "django = 1${CURRENT_VERSION}"${EOL}]${EOL}` ); - }); + }) - it('should add version at path', async () => { + it('should handle example cargo.toml file', async () => { const options = { [NAMESPACE]: { - out: { file: './missing_path_version.toml', path: 'project.version' } + out: { file: './cargo.toml', path: 'package.version' } } }; const plugin = await factory(Bumper, { NAMESPACE, options }); await runTasks(plugin); - assert.equal( - readFile('./missing_path_version.toml'), - `[project]${EOL}name = "foo"${EOL}version = "${NEW_VERSION}"${EOL}[tool.commitizen]${EOL}version = "${CURRENT_VERSION}"${EOL}` + readFile('./cargo.toml'), + `[workspace]${EOL}${EOL}[package]${EOL}name = "hello_world"${EOL}version = "${NEW_VERSION}"${EOL}authors = [ "Alice ", "Bob " ]${EOL}${EOL}[dependencies]${EOL}time = "0.1.12"${EOL}` ); - }) + }); - it('should noop when the target version is already at the new version', async () => { + it('should handle example pyproject.toml file', async () => { const options = { [NAMESPACE]: { - out: { file: './at_current_version.toml', path: 'project.version' } + out: { file: './pyproject.toml', path: 'project.version' } } }; const plugin = await factory(Bumper, { NAMESPACE, options }); await runTasks(plugin); - assert.equal( - readFile('./at_current_version.toml'), - `[project]${EOL}name = "foo"${EOL}version = "${NEW_VERSION}"${EOL}dependencies = [${EOL} "django = 1${CURRENT_VERSION}"${EOL}]${EOL}` + readFile('./pyproject.toml'), + `[project]${EOL}name = "foo"${EOL}version = "${NEW_VERSION}"${EOL}authors = [{ name = "Alice", email = "a@example.com" }]${EOL}dependencies = [${EOL} "django = 1${CURRENT_VERSION}"${EOL}]${EOL}[tool.commitizen]${EOL}version = "${CURRENT_VERSION}"${EOL}` ); - }) + }); }); From 9760ee9a04b893249d764b8e8481876e3dff81a7 Mon Sep 17 00:00:00 2001 From: James Barr Date: Sat, 16 May 2026 23:40:49 +0100 Subject: [PATCH 4/4] test: refactor toml test to reduce boilerplate --- test/toml.test.js | 148 ++++++++++++++++++++-------------------------- 1 file changed, 65 insertions(+), 83 deletions(-) diff --git a/test/toml.test.js b/test/toml.test.js index 9a81cd6..214f63f 100644 --- a/test/toml.test.js +++ b/test/toml.test.js @@ -20,6 +20,17 @@ mock({ './pyproject.toml': `[project]${EOL}name = "foo"${EOL}version = "${CURRENT_VERSION}"${EOL}authors = [{ name = "Alice", email = "a@example.com" }]${EOL}dependencies = [${EOL} "django = 1${CURRENT_VERSION}"${EOL}]${EOL}[tool.commitizen]${EOL}version = "${CURRENT_VERSION}"${EOL}`, }); +const readFilePostBumperTasks = async (namespaceOptions) => { + const options = { + [NAMESPACE]: namespaceOptions + } + + const plugin = await factory(Bumper, { NAMESPACE, options }); + await runTasks(plugin); + + return readFile(namespaceOptions.out.file) +} + describe('toml file', { concurrency: true }, () => { it('should return latest version', async () => { const options = { @@ -31,138 +42,109 @@ describe('toml file', { concurrency: true }, () => { }); it('should add version at path when missing', async () => { - const options = { - [NAMESPACE]: { - out: { - file: './without_target_version.toml', - type: 'text/toml', - path: 'tool.test.version' - } + const namespaceOptions = { + out: { + file: './without_target_version.toml', + type: 'text/toml', + path: 'tool.test.version' } }; - const plugin = await factory(Bumper, { NAMESPACE, options }); - await runTasks(plugin); - assert.equal(readFile('./without_target_version.toml'), `[tool.test]${EOL}version = "${NEW_VERSION}"${EOL}[tool.ignored]${EOL}version = "${CURRENT_VERSION}"${EOL}`); + const contents = await readFilePostBumperTasks(namespaceOptions) + assert.equal(contents, `[tool.test]${EOL}version = "${NEW_VERSION}"${EOL}[tool.ignored]${EOL}version = "${CURRENT_VERSION}"${EOL}`); }); it('should update version at path', async () => { - const options = { - [NAMESPACE]: { - out: { - file: './foo.toml', - type: 'text/toml', - path: 'tool.test.version' - } + const namespaceOptions = { + out: { + file: './foo.toml', + type: 'text/toml', + path: 'tool.test.version' } }; - const plugin = await factory(Bumper, { NAMESPACE, options }); - await runTasks(plugin); - assert.equal(readFile('./foo.toml'), `[tool.test]${EOL}version = "${NEW_VERSION}"${EOL}`); + const contents = await readFilePostBumperTasks(namespaceOptions); + assert.equal(contents, `[tool.test]${EOL}version = "${NEW_VERSION}"${EOL}`); }); it('should update versions at multiple paths', async () => { - const options = { - [NAMESPACE]: { - out: { - file: './with_multiple_version.toml', - type: 'text/toml', - path: ['project.version', 'tool.test.version'] - } + const namespaceOptions = { + out: { + file: './with_multiple_version.toml', + type: 'text/toml', + path: ['project.version', 'tool.test.version'] } }; - const plugin = await factory(Bumper, { NAMESPACE, options }); - await runTasks(plugin); - assert.equal(readFile('./with_multiple_version.toml'), `[tool.test]${EOL}version = "${NEW_VERSION}"${EOL}[project]${EOL}version = "${NEW_VERSION}"${EOL}`); + const contents = await readFilePostBumperTasks(namespaceOptions); + assert.equal(contents, `[tool.test]${EOL}version = "${NEW_VERSION}"${EOL}[project]${EOL}version = "${NEW_VERSION}"${EOL}`); }); it('should write without defining the type', async () => { - const options = { - [NAMESPACE]: { out: { file: './foo.toml', path: 'tool.test.version' } } + const namespaceOptions = { + out: { file: './foo.toml', path: 'tool.test.version' } }; - const plugin = await factory(Bumper, { NAMESPACE, options }); - await runTasks(plugin); - assert.equal(readFile('./foo.toml'), `[tool.test]${EOL}version = "${NEW_VERSION}"${EOL}`); + const contents = await readFilePostBumperTasks(namespaceOptions); + assert.equal(contents, `[tool.test]${EOL}version = "${NEW_VERSION}"${EOL}`); }); it('should read/write', async () => { - const options = { - [NAMESPACE]: { - in: { file: './foo.toml', type: 'application/toml', path: 'tool.test.version' }, - out: { file: './foo.toml', type: 'application/toml', path: 'tool.test.version' } - } + const namespaceOptions = { + in: { file: './foo.toml', type: 'application/toml', path: 'tool.test.version' }, + out: { file: './foo.toml', type: 'application/toml', path: 'tool.test.version' } }; - const plugin = await factory(Bumper, { NAMESPACE, options }); - await runTasks(plugin); - assert.equal(readFile('./foo.toml'), `[tool.test]${EOL}version = "${NEW_VERSION}"${EOL}`); + const contents = await readFilePostBumperTasks(namespaceOptions); + assert.equal(contents, `[tool.test]${EOL}version = "${NEW_VERSION}"${EOL}`); }); it('should read/write without defining the type', async () => { - const options = { - [NAMESPACE]: { - in: { file: './foo.toml', path: 'tool.test.version' }, - out: { file: './foo.toml', path: 'tool.test.version' } - } + const namespaceOptions = { + in: { file: './foo.toml', path: 'tool.test.version' }, + out: { file: './foo.toml', path: 'tool.test.version' } }; - const plugin = await factory(Bumper, { NAMESPACE, options }); - await runTasks(plugin); - assert.equal(readFile('./foo.toml'), `[tool.test]${EOL}version = "${NEW_VERSION}"${EOL}`); + const contents = await readFilePostBumperTasks(namespaceOptions); + assert.equal(contents, `[tool.test]${EOL}version = "${NEW_VERSION}"${EOL}`); }); it('should read/write without formatting', async () => { - const options = { - [NAMESPACE]: { - in: { file: './with_comments_and_formatting.toml', path: 'tool.test.version' }, - out: { file: './with_comments_and_formatting.toml', path: 'tool.test.version' } - } + const namespaceOptions = { + in: { file: './with_comments_and_formatting.toml', path: 'tool.test.version' }, + out: { file: './with_comments_and_formatting.toml', path: 'tool.test.version' } }; - const plugin = await factory(Bumper, { NAMESPACE, options }); - await runTasks(plugin); + const contents = await readFilePostBumperTasks(namespaceOptions); assert.equal( - readFile('./with_comments_and_formatting.toml'), + contents, `# Lead with some comments${EOL}${EOL}[workspace]${EOL}${EOL}${EOL}[tool.test]${EOL}name = "hello_world"${EOL}version = "${NEW_VERSION}"${EOL}` ); }); it('should noop when the target version is already at the new version', async () => { - const options = { - [NAMESPACE]: { - out: { file: './at_current_version.toml', path: 'tool.test.version' } - } + const namespaceOptions = { + out: { file: './at_current_version.toml', path: 'tool.test.version' } }; - const plugin = await factory(Bumper, { NAMESPACE, options }); - await runTasks(plugin); - + const contents = await readFilePostBumperTasks(namespaceOptions); assert.equal( - readFile('./at_current_version.toml'), + contents, `[tool.test]${EOL}version = "${NEW_VERSION}"${EOL}dependencies = [${EOL} "django = 1${CURRENT_VERSION}"${EOL}]${EOL}` ); - }) + }); it('should handle example cargo.toml file', async () => { - const options = { - [NAMESPACE]: { - out: { file: './cargo.toml', path: 'package.version' } - } + const namespaceOptions = { + out: { file: './cargo.toml', path: 'package.version' } }; - const plugin = await factory(Bumper, { NAMESPACE, options }); - await runTasks(plugin); + const contents = await readFilePostBumperTasks(namespaceOptions); assert.equal( - readFile('./cargo.toml'), + contents, `[workspace]${EOL}${EOL}[package]${EOL}name = "hello_world"${EOL}version = "${NEW_VERSION}"${EOL}authors = [ "Alice ", "Bob " ]${EOL}${EOL}[dependencies]${EOL}time = "0.1.12"${EOL}` ); }); it('should handle example pyproject.toml file', async () => { - const options = { - [NAMESPACE]: { - out: { file: './pyproject.toml', path: 'project.version' } - } + const namespaceOptions = { + out: { file: './pyproject.toml', path: 'project.version' } }; - const plugin = await factory(Bumper, { NAMESPACE, options }); - await runTasks(plugin); + const contents = await readFilePostBumperTasks(namespaceOptions); assert.equal( - readFile('./pyproject.toml'), + contents, `[project]${EOL}name = "foo"${EOL}version = "${NEW_VERSION}"${EOL}authors = [{ name = "Alice", email = "a@example.com" }]${EOL}dependencies = [${EOL} "django = 1${CURRENT_VERSION}"${EOL}]${EOL}[tool.commitizen]${EOL}version = "${CURRENT_VERSION}"${EOL}` ); }); -}); +}); \ No newline at end of file