From 241abc88e11bfa4466f619c04d41d99ddac8a170 Mon Sep 17 00:00:00 2001 From: Matt Clarkson Date: Mon, 26 Feb 2024 10:21:20 +0000 Subject: [PATCH 1/5] refactor: move `expression` to a class --- argument.mjs | 49 ++++++++++++++++++++++++++++++++++++++++++ expression.mjs | 29 +++++++++++++++++++++++++ package-lock.json | 9 ++++---- package.json | 7 ++++-- plugin.mjs | 32 ++++++++------------------- test/argument.test.mjs | 41 +++++++++++++++++++++++++++++++++++ 6 files changed, 137 insertions(+), 30 deletions(-) create mode 100644 argument.mjs create mode 100644 expression.mjs create mode 100644 test/argument.test.mjs diff --git a/argument.mjs b/argument.mjs new file mode 100644 index 0000000..b7f41ee --- /dev/null +++ b/argument.mjs @@ -0,0 +1,49 @@ +import {template} from 'lodash-es'; + +export default class Argument { + #argument; + #value; + + constructor(...args) { + switch (args.length) { + case 1: { + [this.#value] = args; + break; + } + + case 2: { + [this.#argument, this.#value] = args; + break; + } + + default: { + throw new Error(`Unsupported number of arguments: ${args}`); + } + } + } + + get argument() { + return this.#argument; + } + + get value() { + return this.#value; + } + + [Symbol.toPrimitive]() { + return this.value; + } + + get template() { + return template(this.value); + } + + async render(ctx) { + const rendered = this.template(ctx); + if (this.argument !== undefined) { + return [`${this.argument}`, `${rendered}`]; + } + + return `${rendered}`; + } +} diff --git a/expression.mjs b/expression.mjs new file mode 100644 index 0000000..ce6515e --- /dev/null +++ b/expression.mjs @@ -0,0 +1,29 @@ +import SemanticReleaseError from '@semantic-release/error'; +import Argument from './argument.mjs'; + +export default class Expression extends Argument { + constructor(value) { + if (typeof value !== 'string') { + throw new SemanticReleaseError( + '`asset.expression` must be a string', + 'EYQCFG', + `${value} (${typeof value})`, + ); + } + + super('--expression', value); + } + + async render(ctx) { + try { + return await super.render(ctx); + } catch (error) { + throw new SemanticReleaseError( + '`asset.expression` failed to be templated', + 'EYQCFG', + `${error}`, + ); + } + } +} + diff --git a/package-lock.json b/package-lock.json index 06bf7b1..eb83bac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@commitlint/config-conventional": "^18", "@semantic-release/changelog": "^6", "@semantic-release/commit-analyzer": "^11", - "@semantic-release/config-gitlab-npm": "^1", + "@semantic-release/config-gitlab-npm": "^1.1.2", "@semantic-release/config-release-channels": "^1", "@semantic-release/exec": "^6", "@semantic-release/git": "^10", @@ -1228,11 +1228,10 @@ } }, "node_modules/@semantic-release/config-gitlab-npm": { - "version": "1.1.0", - "resolved": "https://gitlab.arm.com/semantic-release/config-gitlab-npm/-/releases/v1.1.0/downloads/package.tar.gz", - "integrity": "sha512-Cejn62E96XGK5q40exRqI56TLVRbRKX6DXW9fBpaKqeCDl37w2ohuHWa/eaI6Q+LihQ5ILkBVeQSqfzwISzePQ==", + "version": "1.1.2", + "resolved": "https://gitlab.arm.com/api/v4/projects/407/packages/npm/@semantic-release/config-gitlab-npm/-/@semantic-release/config-gitlab-npm-1.1.2.tgz", + "integrity": "sha1-FBSPpOPyuDm62dtMBDZDmJT/gFA=", "dev": true, - "license": "MIT", "peerDependencies": { "@semantic-release/changelog": ">=4", "@semantic-release/commit-analyzer": ">=9", diff --git a/package.json b/package.json index afae142..9c350ba 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "modules": "plugin.mjs", "files": [ "plugin.js", - "plugin.mjs", + "*.mjs", "bin/*" ], "scripts": { @@ -77,9 +77,12 @@ "c8": { "100": true, "include": [ - "plugin.mjs", + "*.mjs", "plugin.js" ], + "exclude": [ + "prepublish.mjs" + ], "reporter": [ "text", "html", diff --git a/plugin.mjs b/plugin.mjs index 5be0e79..e6afb9d 100644 --- a/plugin.mjs +++ b/plugin.mjs @@ -4,9 +4,9 @@ import path from 'node:path'; import {fileURLToPath} from 'node:url'; import SemanticReleaseError from '@semantic-release/error'; import debug from 'debug'; -import {template} from 'lodash-es'; import {$} from 'execa'; import which from 'which'; +import Expression from './expression.mjs'; debug('semantic-release:yq'); @@ -86,13 +86,7 @@ export async function verifyConditions(pluginConfig, context) { ); } - if (typeof expression !== 'string') { - throw new SemanticReleaseError( - '`asset.expression` must be a string', - 'EYQCFG', - `${expression} (${typeof expression})`, - ); - } + expression = new Expression(expression); const location = path.join(cwd, filepath); @@ -118,21 +112,13 @@ export async function verifyConditions(pluginConfig, context) { ...context, }; - const rendered = (() => { - try { - return template(expression)(ctx); - } catch (error) { - throw new SemanticReleaseError( - '`asset.expression` failed to be templated', - 'EYQCFG', - `${error}`, - ); - } - })(); - try { - await $`${yq} --expression ${rendered} ${location}`; + await $`${yq} ${await expression.render(ctx)} ${location}`; } catch (error) { + if (error instanceof SemanticReleaseError) { + throw error; + } + throw new SemanticReleaseError( 'Running `yq` failed', 'EYQ', @@ -153,8 +139,8 @@ export async function prepare(pluginConfig, context) { const updates = assets.map(({filepath, expression}) => (async () => { const location = path.join(cwd, filepath); - const rendered = template(expression)(context); - await $`${yq} --inplace --expression ${rendered} ${location}`; + expression = new Expression(expression); + await $`${yq} --inplace ${await expression.render(context)} ${location}`; logger.success('Wrote `%s`', location); })()); diff --git a/test/argument.test.mjs b/test/argument.test.mjs new file mode 100644 index 0000000..9487c94 --- /dev/null +++ b/test/argument.test.mjs @@ -0,0 +1,41 @@ +import test from 'ava'; +import Argument from '../argument.mjs'; + +test('`Argument` cannot be constructed with zero arguments', t => { + t.throws(() => new Argument()); +}); + +test('`Argument` can be constructed with one argument', t => { + const argument = new Argument('one'); + t.is(argument.argument, undefined); + t.is(argument.value, 'one'); +}); + +test('`Argument` can be constructed with two arguments', t => { + const argument = new Argument('one', 'two'); + t.is(argument.argument, 'one'); + t.is(argument.value, 'two'); +}); + +test('`Argument` cannot be constructed with three arguments', t => { + t.throws(() => new Argument('one', 'two', 'three')); +}); + +test('`Argument` can be converted to a primitive', t => { + const argument = new Argument('one'); + t.is(`${argument}`, 'one'); +}); + +test('`Argument` can be rendered with one argument', async t => { + // eslint-disable-next-line no-template-curly-in-string + const argument = new Argument('${one}'); + const rendered = await argument.render({one: 1}); + t.is(rendered, '1'); +}); + +test('`Argument` can be rendered with two arguments', async t => { + // eslint-disable-next-line no-template-curly-in-string + const argument = new Argument('--yes', '${one}'); + const rendered = await argument.render({one: 1}); + t.deepEqual(rendered, ['--yes', '1']); +}); -- GitLab From f1281f84781f342ed126d77b79d31ae806d56294 Mon Sep 17 00:00:00 2001 From: Matt Clarkson Date: Mon, 26 Feb 2024 11:02:10 +0000 Subject: [PATCH 2/5] refactor: move `filepath` to a class --- file-path.mjs | 48 +++++++++++++++++++++++++++++++++++++++++ plugin.mjs | 29 ++++++++----------------- test/file-path.test.mjs | 17 +++++++++++++++ 3 files changed, 74 insertions(+), 20 deletions(-) create mode 100644 file-path.mjs create mode 100644 test/file-path.test.mjs diff --git a/file-path.mjs b/file-path.mjs new file mode 100644 index 0000000..e2f075f --- /dev/null +++ b/file-path.mjs @@ -0,0 +1,48 @@ +import {access, constants} from 'node:fs/promises'; +import path from 'node:path'; +import SemanticReleaseError from '@semantic-release/error'; +import Argument from './argument.mjs'; + +export default class FilePath extends Argument { + constructor(value) { + if (typeof value !== 'string') { + throw new SemanticReleaseError( + '`asset.filepath` must be a string', + 'EYQCFG', + `${value} (${typeof value})`, + ); + } + + super(value); + } + + async #rendered(ctx) { + try { + return await super.render(ctx); + } catch (error) { + throw new SemanticReleaseError( + '`asset.filepath` failed to be templated', + 'EYQCFG', + `${error}`, + ); + } + } + + async render(ctx) { + const location = path.join(ctx.cwd, await this.#rendered(ctx)); + + try { + // eslint-disable-next-line no-bitwise + await access(location, constants.R_OK | constants.W_OK); + } catch (error) { + throw new SemanticReleaseError( + `Insufficient file access for \`${this.value}\``, + 'EYQCFG', + `${error}`, + ); + } + + return location; + } +} + diff --git a/plugin.mjs b/plugin.mjs index e6afb9d..9026c77 100644 --- a/plugin.mjs +++ b/plugin.mjs @@ -6,6 +6,7 @@ import SemanticReleaseError from '@semantic-release/error'; import debug from 'debug'; import {$} from 'execa'; import which from 'which'; +import FilePath from './file-path.mjs'; import Expression from './expression.mjs'; debug('semantic-release:yq'); @@ -50,7 +51,7 @@ const YQ = await (async () => { export async function verifyConditions(pluginConfig, context) { const {assets, yq = YQ} = pluginConfig; - const {cwd, logger} = context; + const {logger} = context; /* c8 ignore next */ if (yq === undefined) { @@ -86,21 +87,9 @@ export async function verifyConditions(pluginConfig, context) { ); } + filepath = new FilePath(filepath); expression = new Expression(expression); - const location = path.join(cwd, filepath); - - try { - // eslint-disable-next-line no-bitwise - await access(location, constants.R_OK | constants.W_OK); - } catch (error) { - throw new SemanticReleaseError( - `Insufficient file access for \`${filepath}\``, - 'EYQCFG', - `${error}`, - ); - } - const ctx = { nextRelease: { type: 'patch', @@ -113,7 +102,7 @@ export async function verifyConditions(pluginConfig, context) { }; try { - await $`${yq} ${await expression.render(ctx)} ${location}`; + await $`${yq} ${await expression.render(ctx)} ${await filepath.render(ctx)}`; } catch (error) { if (error instanceof SemanticReleaseError) { throw error; @@ -126,7 +115,7 @@ export async function verifyConditions(pluginConfig, context) { ); } - logger.success('Validated `%s`', location); + logger.success('Validated `%s`', await filepath.render(ctx)); })()); await Promise.all(checks); @@ -135,13 +124,13 @@ export async function verifyConditions(pluginConfig, context) { export async function prepare(pluginConfig, context) { const {assets, yq = YQ} = pluginConfig; - const {cwd, logger} = context; + const {logger, ...ctx} = context; const updates = assets.map(({filepath, expression}) => (async () => { - const location = path.join(cwd, filepath); + filepath = new FilePath(filepath); expression = new Expression(expression); - await $`${yq} --inplace ${await expression.render(context)} ${location}`; - logger.success('Wrote `%s`', location); + await $`${yq} --inplace ${await expression.render(context)} ${await filepath.render(ctx)}`; + logger.success('Wrote `%s`', await filepath.render(ctx)); })()); await Promise.all(updates); diff --git a/test/file-path.test.mjs b/test/file-path.test.mjs new file mode 100644 index 0000000..bde922d --- /dev/null +++ b/test/file-path.test.mjs @@ -0,0 +1,17 @@ +import test from 'ava'; +import FilePath from '../file-path.mjs'; + +test('`FilePath` cannot be constructed with a object', t => { + t.throws(() => new FilePath({})); +}); + +test('`FilePath` throws with an invalid templated filepath', async t => { + // eslint-disable-next-line no-template-curly-in-string + const argument = new FilePath('${one}'); + await t.throwsAsync(async () => argument.render({})); +}); + +test('`FilePath` throws with an invalid filepath', async t => { + const argument = new FilePath('this-does-not-exist'); + await t.throwsAsync(async () => argument.render({})); +}); -- GitLab From 77cd03df16a1bfa563e364f0bd3bcb44998fedc7 Mon Sep 17 00:00:00 2001 From: Matt Clarkson Date: Mon, 26 Feb 2024 11:29:47 +0000 Subject: [PATCH 3/5] refactor: move `yq` search to ECMAscript module --- plugin.mjs | 49 ++++-------------------------------------- test/yq.test.mjs | 7 ++++++ yq.mjs | 55 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 45 deletions(-) create mode 100644 test/yq.test.mjs create mode 100644 yq.mjs diff --git a/plugin.mjs b/plugin.mjs index 9026c77..b2cf172 100644 --- a/plugin.mjs +++ b/plugin.mjs @@ -1,62 +1,21 @@ -import {access, constants, readdir} from 'node:fs/promises'; import os from 'node:os'; -import path from 'node:path'; -import {fileURLToPath} from 'node:url'; import SemanticReleaseError from '@semantic-release/error'; import debug from 'debug'; import {$} from 'execa'; -import which from 'which'; import FilePath from './file-path.mjs'; import Expression from './expression.mjs'; +import find, {bins as available} from './yq.mjs'; debug('semantic-release:yq'); -const BIN = (() => { - const file = fileURLToPath(import.meta.url); - const dirname = path.dirname(file); - const bin = path.join(dirname, 'bin'); - return bin; -})(); - -const YQ = await (async () => { - const arch = { - arm: 'arm', - arm64: 'arm64', - ppc64: 'ppc64', - x64: 'amd64', - s390: 's390', - s390x: 's390x', - mips: 'mips', - riscv64: 'riscv64', - }[os.arch()]; - const platform = { - darwin: 'darwin', - linux: 'linux', - freebsd: 'freebsd', - openbsd: 'openbsd', - netbsd: 'netbsd', - win32: 'windows', - }[os.platform()]; - const extension = platform === 'windows' ? /* c8 ignore next */ '.exe' : ''; - const basename = `yq_${platform}_${arch}${extension}`; - const filepath = path.join(BIN, basename); - try { - await access(filepath, constants.X_OK); - return filepath; - /* c8 ignore next 3 */ - } catch { - return which('yq').catch(() => undefined); - } -})(); - export async function verifyConditions(pluginConfig, context) { - const {assets, yq = YQ} = pluginConfig; + const {assets, yq = await find()} = pluginConfig; const {logger} = context; /* c8 ignore next */ if (yq === undefined) { /* c8 ignore next 6 */ - const bins = await readdir(BIN).catch(() => []); + const bins = await available().catch(() => []); throw new SemanticReleaseError( 'No `yq` binary available', 'EYQCFG', @@ -123,7 +82,7 @@ export async function verifyConditions(pluginConfig, context) { } export async function prepare(pluginConfig, context) { - const {assets, yq = YQ} = pluginConfig; + const {assets, yq = await find()} = pluginConfig; const {logger, ...ctx} = context; const updates = assets.map(({filepath, expression}) => (async () => { diff --git a/test/yq.test.mjs b/test/yq.test.mjs new file mode 100644 index 0000000..7d7fad4 --- /dev/null +++ b/test/yq.test.mjs @@ -0,0 +1,7 @@ +import test from 'ava'; +import {bins} from '../yq.mjs'; + +test('`yq.bins` can find some binaries', async t => { + const binaries = await bins(); + t.not(binaries.length, 0); +}); diff --git a/yq.mjs b/yq.mjs new file mode 100644 index 0000000..1390acb --- /dev/null +++ b/yq.mjs @@ -0,0 +1,55 @@ +import {stat, readdir, access, constants} from 'node:fs/promises'; +import {fileURLToPath} from 'node:url'; +import os from 'node:os'; +import path from 'node:path'; +import which from 'which'; + +const BIN = (() => { + const file = fileURLToPath(import.meta.url); + const dirname = path.dirname(file); + const bin = path.join(dirname, 'bin'); + return bin; +})(); + +const ARCH = { + arm: 'arm', + arm64: 'arm64', + ppc64: 'ppc64', + x64: 'amd64', + s390: 's390', + s390x: 's390x', + mips: 'mips', + riscv64: 'riscv64', +}[os.arch()]; + +const PLATFORM = { + darwin: 'darwin', + linux: 'linux', + freebsd: 'freebsd', + openbsd: 'openbsd', + netbsd: 'netbsd', + win32: 'windows', +}[os.platform()]; + +export async function bins(bin = BIN) { + const possibles = await readdir(bin); + const stats = await Promise.all(possibles.map(filepath => stat(path.join(bin, filepath)))); + return possibles.filter((_, i) => { + const status = stats[i]; + // eslint-disable-next-line no-bitwise + return status.isFile() && (status.mode & constants.S_IXUSR); + }); +} + +export default async function yq(bin = BIN, platform = PLATFORM, arch = ARCH) { + const extension = platform === 'windows' ? /* c8 ignore next */ '.exe' : ''; + const basename = `yq_${platform}_${arch}${extension}`; + const filepath = path.join(bin, basename); + try { + await access(filepath, constants.X_OK); + return filepath; + /* c8 ignore next 3 */ + } catch { + return which('yq').catch(() => undefined); + } +} -- GitLab From ef599708cea61905b5275b48c8d0bbfa7e2a4dba Mon Sep 17 00:00:00 2001 From: Matt Clarkson Date: Mon, 26 Feb 2024 16:28:05 +0000 Subject: [PATCH 4/5] refactor: move `assets` into a class --- asset.mjs | 49 ++++++++++++++++++++++++++++++++ assets.mjs | 32 +++++++++++++++++++++ plugin.mjs | 68 +++++++++++++++++---------------------------- test/asset.test.mjs | 10 +++++++ 4 files changed, 116 insertions(+), 43 deletions(-) create mode 100644 asset.mjs create mode 100644 assets.mjs create mode 100644 test/asset.test.mjs diff --git a/asset.mjs b/asset.mjs new file mode 100644 index 0000000..27ed6e6 --- /dev/null +++ b/asset.mjs @@ -0,0 +1,49 @@ +import SemanticReleaseError from '@semantic-release/error'; +import FilePath from './file-path.mjs'; +import Expression from './expression.mjs'; + +export default class Asset { + #expression; + #filepath; + + constructor(value) { + if (typeof value !== 'object' || Array.isArray(value)) { + throw new SemanticReleaseError( + 'An asset must be an object', + 'EYQCFG', + `${value} (${typeof value})`, + ); + } + + const {expression, filepath} = value; + this.#expression = new Expression(expression); + this.#filepath = new FilePath(filepath); + } + + get expression() { + return this.#expression; + } + + get filepath() { + return this.#filepath; + } + + [Symbol.iterator] = function * () { + yield this.expression; + yield this.filepath; + }; + + get #array() { + return Array.from(this); + } + + map(...args) { + return this.#array.map(...args); + } + + async render(ctx) { + const promises = this.map(a => a.render(ctx)); + const values = await Promise.all(promises); + return values.flat(); + } +} diff --git a/assets.mjs b/assets.mjs new file mode 100644 index 0000000..b842774 --- /dev/null +++ b/assets.mjs @@ -0,0 +1,32 @@ +import SemanticReleaseError from '@semantic-release/error'; +import Asset from './asset.mjs'; + +export default class Assets { + #value; + + constructor(value) { + if (!Array.isArray(value)) { + throw new SemanticReleaseError( + '`assets` must be an array', + 'EYQCFG', + `${value} (${typeof value})`, + ); + } + + this.#value = value; + } + + [Symbol.iterator] = function * () { + for (const asset of this.#value) { + yield new Asset(asset); + } + }; + + get #array() { + return Array.from(this); + } + + map(...args) { + return this.#array.map(...args); + } +} diff --git a/plugin.mjs b/plugin.mjs index b2cf172..d3f4901 100644 --- a/plugin.mjs +++ b/plugin.mjs @@ -2,15 +2,14 @@ import os from 'node:os'; import SemanticReleaseError from '@semantic-release/error'; import debug from 'debug'; import {$} from 'execa'; -import FilePath from './file-path.mjs'; -import Expression from './expression.mjs'; import find, {bins as available} from './yq.mjs'; +import Assets from './assets.mjs'; debug('semantic-release:yq'); export async function verifyConditions(pluginConfig, context) { - const {assets, yq = await find()} = pluginConfig; - const {logger} = context; + const {assets: value, yq = await find()} = pluginConfig; + const {logger, ...rest} = context; /* c8 ignore next */ if (yq === undefined) { @@ -29,39 +28,22 @@ export async function verifyConditions(pluginConfig, context) { ); } - if (!Array.isArray(assets)) { - throw new SemanticReleaseError( - '`assets` must be an array', - 'EYQCFG', - `${assets} (${typeof assets})`, - ); - } - - const checks = assets.map(({filepath, expression}) => (async () => { - if (typeof filepath !== 'string') { - throw new SemanticReleaseError( - '`asset.filepath` must be a string', - 'EYQCFG', - `${filepath} (${typeof filepath})`, - ); - } + const assets = new Assets(value); - filepath = new FilePath(filepath); - expression = new Expression(expression); - - const ctx = { - nextRelease: { - type: 'patch', - version: '0.0.0', - gitHead: '0123456789abcedf0123456789abcdef12345678', - gitTag: 'v0.0.0', - notes: 'placeholder', - }, - ...context, - }; + const ctx = { + nextRelease: { + type: 'patch', + version: '0.0.0', + gitHead: '0123456789abcedf0123456789abcdef12345678', + gitTag: 'v0.0.0', + notes: 'placeholder', + }, + ...rest, + }; + const checks = assets.map(async asset => { try { - await $`${yq} ${await expression.render(ctx)} ${await filepath.render(ctx)}`; + await $`${yq} ${await asset.render(ctx)}`; } catch (error) { if (error instanceof SemanticReleaseError) { throw error; @@ -74,23 +56,23 @@ export async function verifyConditions(pluginConfig, context) { ); } - logger.success('Validated `%s`', await filepath.render(ctx)); - })()); + logger.success('Validated `%s`', await asset.filepath.render(ctx)); + }); await Promise.all(checks); logger.success('Validated `assets` configuration'); } export async function prepare(pluginConfig, context) { - const {assets, yq = await find()} = pluginConfig; + const {assets: value, yq = await find()} = pluginConfig; const {logger, ...ctx} = context; - const updates = assets.map(({filepath, expression}) => (async () => { - filepath = new FilePath(filepath); - expression = new Expression(expression); - await $`${yq} --inplace ${await expression.render(context)} ${await filepath.render(ctx)}`; - logger.success('Wrote `%s`', await filepath.render(ctx)); - })()); + const assets = new Assets(value); + + const updates = assets.map(async asset => { + await $`${yq} --inplace ${await asset.render(ctx)}`; + logger.success('Wrote `%s`', await asset.filepath.render(ctx)); + }); await Promise.all(updates); } diff --git a/test/asset.test.mjs b/test/asset.test.mjs new file mode 100644 index 0000000..58162ef --- /dev/null +++ b/test/asset.test.mjs @@ -0,0 +1,10 @@ +import test from 'ava'; +import Asset from '../asset.mjs'; + +test('`Asset` cannot be constructed with a string', t => { + t.throws(() => new Asset('invalid')); +}); + +test('`Asset` cannot be constructed with an array', t => { + t.throws(() => new Asset([])); +}); -- GitLab From 8d3247af83d53d793e3891cb5350d2073de5c193 Mon Sep 17 00:00:00 2001 From: Matt Clarkson Date: Mon, 26 Feb 2024 16:46:42 +0000 Subject: [PATCH 5/5] feat: add `assets.frontMatter` --- README.md | 48 +++++++++++++++++++++++++++----------- argument.mjs | 8 +++++++ asset.mjs | 10 +++++++- front-matter.mjs | 26 +++++++++++++++++++++ test/front-matter.test.mjs | 10 ++++++++ test/integration.test.mjs | 17 +++++++++++--- 6 files changed, 101 insertions(+), 18 deletions(-) create mode 100644 front-matter.mjs create mode 100644 test/front-matter.test.mjs diff --git a/README.md b/README.md index 659ea77..fc2fcf3 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ # yq -A `semantic-release` plugin to update JSON, YAML, TOML and more files with [`yq`]. +A `semantic-release` plugin to update JSON, YAML, TOML and more files with +[`yq`]. -| Step | Description | -| ---- | ----------- | +| Step | Description | +| ---------------- | ------------------------------------------------------------------- | | verifyConditions | Verify that each file is writable and the `yq` expression is valid. | -| prepare | Updates each file in-place with the `yq` expression provided. | +| prepare | Updates each file in-place with the `yq` expression provided. | ## Getting Started @@ -79,6 +80,21 @@ plugins: .version = "${nextRelease.version}" ``` +### `assets[*].frontMatter` + +Determines if the `expression` should operate on the YAML [front +matter][front-matter]. + +```yaml +plugins: + - path: "@semantic-release/yq" + assets: + - filepath: "./some/file.yaml" + frontMatter: "process" + expression: |- + .version = "${nextRelease.version}" +``` + ### `yq` The NPM package contains a certain set of hermetic `yq` binaries. @@ -86,18 +102,22 @@ The NPM package contains a certain set of hermetic `yq` binaries. To keep the package size small, not _all_ `yq` binaries are provided. Options are: - - Provide a `yq` binaries on the `$PATH` - - Set the `yq` configuration variable - ```yaml - plugins: - - path: "@semantic-release/yq" - yq: "/usr/bin/yq" - ``` - - Request for more `yq` binaries to be included in the package + +- Provide a `yq` binaries on the `$PATH` +- Set the `yq` configuration variable + ```yaml + plugins: + - path: "@semantic-release/yq" + yq: "/usr/bin/yq" + ``` +- Request for more `yq` binaries to be included in the package Binaries are included in the package with a `prepublish.mjs` download script. [yq]: https://github.com/mikefarah/yq -[quick-usage-guide]: https://github.com/mikefarah/yq?tab=readme-ov-file#quick-usage-guide +[quick-usage-guide]: + https://github.com/mikefarah/yq?tab=readme-ov-file#quick-usage-guide [lodash template]: https://docs-lodash.com/v4/template/ -[substitutions]: https://semantic-release.gitbook.io/semantic-release/developer-guide/js-api#nextrelease +[substitutions]: + https://semantic-release.gitbook.io/semantic-release/developer-guide/js-api#nextrelease +[front-matter]: https://mikefarah.gitbook.io/yq/usage/front-matter diff --git a/argument.mjs b/argument.mjs index b7f41ee..9aff64e 100644 --- a/argument.mjs +++ b/argument.mjs @@ -35,11 +35,19 @@ export default class Argument { } get template() { + if (this.value === undefined) { + return () => undefined; + } + return template(this.value); } async render(ctx) { const rendered = this.template(ctx); + if (rendered === undefined) { + return []; + } + if (this.argument !== undefined) { return [`${this.argument}`, `${rendered}`]; } diff --git a/asset.mjs b/asset.mjs index 27ed6e6..4f57184 100644 --- a/asset.mjs +++ b/asset.mjs @@ -1,10 +1,12 @@ import SemanticReleaseError from '@semantic-release/error'; import FilePath from './file-path.mjs'; import Expression from './expression.mjs'; +import FrontMatter from './front-matter.mjs'; export default class Asset { #expression; #filepath; + #frontMatter; constructor(value) { if (typeof value !== 'object' || Array.isArray(value)) { @@ -15,9 +17,10 @@ export default class Asset { ); } - const {expression, filepath} = value; + const {expression, filepath, frontMatter} = value; this.#expression = new Expression(expression); this.#filepath = new FilePath(filepath); + this.#frontMatter = new FrontMatter(frontMatter); } get expression() { @@ -28,7 +31,12 @@ export default class Asset { return this.#filepath; } + get frontMatter() { + return this.#frontMatter; + } + [Symbol.iterator] = function * () { + yield this.frontMatter; yield this.expression; yield this.filepath; }; diff --git a/front-matter.mjs b/front-matter.mjs new file mode 100644 index 0000000..88e0525 --- /dev/null +++ b/front-matter.mjs @@ -0,0 +1,26 @@ +import SemanticReleaseError from '@semantic-release/error'; +import Argument from './argument.mjs'; + +export default class FrontMatter extends Argument { + static choices = Object.freeze(['extract', 'process']); + + constructor(value) { + if (value === undefined) { + // Pass + } else if (typeof value !== 'string') { + throw new SemanticReleaseError( + '`asset.frontMatter` must be a string', + 'EYQCFG', + `${value} (${typeof value})`, + ); + } else if (!FrontMatter.choices.includes(value)) { + throw new SemanticReleaseError( + `\`asset.frontMatter\` must be one of ${FrontMatter.choices.map(s => `\`${s}\``)}`, + 'EYQCFG', + ); + } + + super('--front-matter', value); + } +} + diff --git a/test/front-matter.test.mjs b/test/front-matter.test.mjs new file mode 100644 index 0000000..bd0408f --- /dev/null +++ b/test/front-matter.test.mjs @@ -0,0 +1,10 @@ +import test from 'ava'; +import FrontMatter from '../front-matter.mjs'; + +test('`FrontMatter` cannot be constructed with an object', t => { + t.throws(() => new FrontMatter({})); +}); + +test('`FrontMatter` cannot be constructed with an invalid choice', t => { + t.throws(() => new FrontMatter('what')); +}); diff --git a/test/integration.test.mjs b/test/integration.test.mjs index 801f256..0152cd0 100644 --- a/test/integration.test.mjs +++ b/test/integration.test.mjs @@ -79,8 +79,6 @@ const success = test.macro(async (t, before, after) => { await t.notThrowsAsync( t.context.m.verifyConditions(t.context.cfg, t.context.ctx), ); - const data = await readFile(t.context.filepath, {encoding: 'utf8'}); - t.is(data, 'version: "1.0.0"\n'); if (after) { await after(t); @@ -112,4 +110,17 @@ test('Throws `EYQ` error when running `yq` failed', failure, async t => { t.context.cfg.assets[0].expression = '.v = nope'; }); -test('Verify and prepare assets', success); +test('Verify and prepare assets', success, undefined, async t => { + const data = await readFile(t.context.filepath, {encoding: 'utf8'}); + t.is(data, 'version: "1.0.0"\n'); +}); + +test('Verify and prepare assets with front matter', success, async t => { + t.context.cfg.assets[0].frontMatter = 'process'; + + const data = await readFile(t.context.filepath, {encoding: 'utf8'}); + await writeFile(t.context.filepath, `version: "0.0.0"\n---\n${data}`); +}, async t => { + const data = await readFile(t.context.filepath, {encoding: 'utf8'}); + t.is(data, 'version: "1.0.0"\n---\nversion: "0.0.0"'); +}); -- GitLab