-module(version_bump@plugins@npm).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/version_bump/plugins/npm.gleam").
-export([set_version/2, plugin/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" The npm publish plugin (plugin name \"npm\").\n"
"\n"
" Mirrors `@semantic-release/npm`. It manages a `package.json` and the npm\n"
" registry across three hooks:\n"
"\n"
" - verify_conditions: there must be a `package.json` in `context.cwd` and an\n"
" `NPM_TOKEN` available (checked in `context.env`, then the process env).\n"
" - prepare: rewrite the `\"version\"` field of `package.json` to the next\n"
" release version, preserving the rest of the file verbatim.\n"
" - publish: run `npm publish` in `context.cwd`, returning a `Release`.\n"
"\n"
" The only PURE function is `set_version`, which performs the `package.json`\n"
" version rewrite as a string transformation so it can be unit-tested without\n"
" any IO. It is exported for that reason.\n"
).
-file("src/version_bump/plugins/npm.gleam", 237).
?DOC(" Shell out to `npm publish`, mapping a non-zero exit to a `PluginError`.\n").
-spec run_npm_publish(binary()) -> {ok, nil} |
{error, version_bump@error:release_error()}.
run_npm_publish(Cwd) ->
case shellout:command(<<"npm"/utf8>>, [<<"publish"/utf8>>], Cwd, []) of
{ok, _} ->
{ok, nil};
{error, {Code, Message}} ->
{error,
{plugin_error,
<<"npm"/utf8>>,
<<<<<<"`npm publish` failed (exit "/utf8,
(erlang:integer_to_binary(Code))/binary>>/binary,
"): "/utf8>>/binary,
(gleam@string:trim(Message))/binary>>}}
end.
-file("src/version_bump/plugins/npm.gleam", 264).
?DOC(
" Extract the next release from the context, failing when the pipeline has not\n"
" determined one (which should never happen by the time `prepare`/`publish`\n"
" run, but is handled rather than asserted).\n"
).
-spec require_next_release(version_bump@context:context()) -> {ok,
version_bump@release:next_release()} |
{error, version_bump@error:release_error()}.
require_next_release(Context) ->
case erlang:element(9, Context) of
{some, Next} ->
{ok, Next};
none ->
{error,
{plugin_error,
<<"npm"/utf8>>,
<<"no next release determined"/utf8>>}}
end.
-file("src/version_bump/plugins/npm.gleam", 216).
?DOC(
" Run `npm publish` in `context.cwd` and report the resulting release. `npm publish`\n"
" is synchronous (a subprocess), so the result is wrapped in an already-resolved\n"
" task to satisfy the asynchronous `publish` contract.\n"
).
-spec publish(version_bump@config:plugin_spec(), version_bump@context:context()) -> version_bump@task:task({ok,
gleam@option:option(version_bump@release:release())} |
{error, version_bump@error:release_error()}).
publish(_, Context) ->
version_bump_task_ffi:resolve(
begin
gleam@result:'try'(
require_next_release(Context),
fun(Next) ->
gleam@result:'try'(
run_npm_publish(erlang:element(2, Context)),
fun(_) ->
{ok,
{some,
{release,
<<"npm"/utf8>>,
none,
erlang:element(2, Next),
erlang:element(4, Next),
erlang:element(6, Next),
<<"npm"/utf8>>}}}
end
)
end
)
end
).
-file("src/version_bump/plugins/npm.gleam", 135).
?DOC(" Write a file, mapping any failure to a `PluginError`.\n").
-spec write_file(binary(), binary()) -> {ok, nil} |
{error, version_bump@error:release_error()}.
write_file(Path, Contents) ->
_pipe = simplifile:write(Path, Contents),
gleam@result:map_error(
_pipe,
fun(Err) ->
{plugin_error,
<<"npm"/utf8>>,
<<<<<<"could not write "/utf8, Path/binary>>/binary, ": "/utf8>>/binary,
(simplifile:describe_error(Err))/binary>>}
end
).
-file("src/version_bump/plugins/npm.gleam", 191).
?DOC(
" Replace only the first occurrence of `needle` in `haystack` with\n"
" `replacement`. Used so that rewriting the top-level `version` never also\n"
" rewrites an identical `\"version\": \"x\"` pair nested deeper in the document.\n"
).
-spec replace_first(binary(), binary(), binary()) -> binary().
replace_first(Haystack, Needle, Replacement) ->
case gleam@string:split_once(Haystack, Needle) of
{ok, {Before, After}} ->
<<<<Before/binary, Replacement/binary>>/binary, After/binary>>;
{error, _} ->
Haystack
end.
-file("src/version_bump/plugins/npm.gleam", 205).
?DOC(
" Escape a string for safe inclusion inside a JSON string literal. A semantic\n"
" version contains only `[0-9A-Za-z.+-]`, so in practice nothing needs\n"
" escaping, but this keeps `set_version` correct for arbitrary input.\n"
).
-spec escape_json_string(binary()) -> binary().
escape_json_string(Value) ->
_pipe = Value,
_pipe@1 = gleam@string:replace(_pipe, <<"\\"/utf8>>, <<"\\\\"/utf8>>),
gleam@string:replace(_pipe@1, <<"\""/utf8>>, <<"\\\""/utf8>>).
-file("src/version_bump/plugins/npm.gleam", 182).
?DOC(
" Compile the regexp matching a JSON `\"version\": \"<value>\"` pair.\n"
"\n"
" Submatch 1 captures everything up to and including the opening quote of the\n"
" value (the key, colon, whitespace, and opening `\"`); submatch 2 captures the\n"
" existing value. Rebuilding `prefix <> new_value <> \"\\\"\"` preserves the\n"
" original key spacing while swapping only the value.\n"
).
-spec version_regexp() -> {ok, gleam@regexp:regexp()} | {error, binary()}.
version_regexp() ->
Pattern = <<"(\"version\"\\s*:\\s*\")((?:\\\\.|[^\"\\\\])*)\""/utf8>>,
_pipe = gleam@regexp:from_string(Pattern),
gleam@result:map_error(
_pipe,
fun(Err) ->
<<"invalid version regexp: "/utf8, (erlang:element(2, Err))/binary>>
end
).
-file("src/version_bump/plugins/npm.gleam", 155).
?DOC(
" Replace the top-level `\"version\"` field in a `package.json` document with\n"
" `version`, leaving the rest of the document byte-for-byte intact.\n"
"\n"
" PURE: a string transformation, no IO. Returns a `PluginError` when no\n"
" `\"version\"` field is present so callers can surface a clear message rather\n"
" than silently producing a package.json without a version.\n"
"\n"
" The match targets the first `\"version\": \"...\"` pair, which in a well-formed\n"
" `package.json` is the top-level package version. Only the quoted value is\n"
" rewritten; surrounding whitespace and formatting are preserved.\n"
).
-spec set_version(binary(), binary()) -> {ok, binary()} |
{error, version_bump@error:release_error()}.
set_version(Package_json, Version) ->
case version_regexp() of
{error, Message} ->
{error, {plugin_error, <<"npm"/utf8>>, Message}};
{ok, Re} ->
case gleam@regexp:scan(Re, Package_json) of
[{match, Matched, [{some, Prefix}, _]} | _] ->
Replacement = <<<<Prefix/binary,
(escape_json_string(Version))/binary>>/binary,
"\""/utf8>>,
{ok, replace_first(Package_json, Matched, Replacement)};
_ ->
{error,
{plugin_error,
<<"npm"/utf8>>,
<<"no \"version\" field found in package.json"/utf8>>}}
end
end.
-file("src/version_bump/plugins/npm.gleam", 124).
?DOC(" Read a file, mapping any failure to a `PluginError`.\n").
-spec read_file(binary()) -> {ok, binary()} |
{error, version_bump@error:release_error()}.
read_file(Path) ->
_pipe = simplifile:read(Path),
gleam@result:map_error(
_pipe,
fun(Err) ->
{plugin_error,
<<"npm"/utf8>>,
<<<<<<"could not read "/utf8, Path/binary>>/binary, ": "/utf8>>/binary,
(simplifile:describe_error(Err))/binary>>}
end
).
-file("src/version_bump/plugins/npm.gleam", 254).
?DOC(" The path to `package.json` within the working directory.\n").
-spec package_json_path(binary()) -> binary().
package_json_path(Cwd) ->
case gleam_stdlib:string_ends_with(Cwd, <<"/"/utf8>>) of
true ->
<<Cwd/binary, "package.json"/utf8>>;
false ->
<<Cwd/binary, "/package.json"/utf8>>
end.
-file("src/version_bump/plugins/npm.gleam", 115).
?DOC(
" Rewrite `package.json`'s `version` to the next release version and write it\n"
" back to disk.\n"
).
-spec prepare(version_bump@config:plugin_spec(), version_bump@context:context()) -> {ok,
nil} |
{error, version_bump@error:release_error()}.
prepare(_, Context) ->
gleam@result:'try'(
require_next_release(Context),
fun(Next) ->
Path = package_json_path(erlang:element(2, Context)),
gleam@result:'try'(
read_file(Path),
fun(Contents) ->
gleam@result:'try'(
set_version(Contents, erlang:element(2, Next)),
fun(Updated) -> write_file(Path, Updated) end
)
end
)
end
).
-file("src/version_bump/plugins/npm.gleam", 88).
?DOC(
" Resolve the npm auth token from `context.env`, falling back to the live process\n"
" environment. Empty/whitespace-only values are treated as absent.\n"
).
-spec npm_token(version_bump@context:context()) -> gleam@option:option(binary()).
npm_token(Context) ->
From_ctx = case gleam_stdlib:map_get(
erlang:element(3, Context),
<<"NPM_TOKEN"/utf8>>
) of
{ok, Value} ->
{some, Value};
{error, _} ->
none
end,
Token = case From_ctx of
{some, Value@1} ->
{some, Value@1};
none ->
case envoy_ffi:get(<<"NPM_TOKEN"/utf8>>) of
{ok, Value@2} ->
{some, Value@2};
{error, _} ->
none
end
end,
case Token of
{some, Value@3} ->
case gleam@string:trim(Value@3) of
<<""/utf8>> ->
none;
Trimmed ->
{some, Trimmed}
end;
none ->
none
end.
-file("src/version_bump/plugins/npm.gleam", 75).
?DOC(
" Verify that an `NPM_TOKEN` is available, checking the context environment\n"
" first and falling back to the live process environment.\n"
).
-spec ensure_npm_token(version_bump@context:context()) -> {ok, nil} |
{error, version_bump@error:release_error()}.
ensure_npm_token(Context) ->
case npm_token(Context) of
{some, _} ->
{ok, nil};
none ->
{error,
{plugin_error,
<<"npm"/utf8>>,
<<"NPM_TOKEN environment variable is not set"/utf8>>}}
end.
-file("src/version_bump/plugins/npm.gleam", 64).
?DOC(" Verify that `package.json` is present in the working directory.\n").
-spec ensure_package_json_exists(binary()) -> {ok, nil} |
{error, version_bump@error:release_error()}.
ensure_package_json_exists(Cwd) ->
Path = package_json_path(Cwd),
case simplifile_erl:is_file(Path) of
{ok, true} ->
{ok, nil};
{ok, false} ->
{error,
{plugin_error,
<<"npm"/utf8>>,
<<"no package.json found at "/utf8, Path/binary>>}};
{error, _} ->
{error,
{plugin_error,
<<"npm"/utf8>>,
<<"no package.json found at "/utf8, Path/binary>>}}
end.
-file("src/version_bump/plugins/npm.gleam", 50).
?DOC(" Ensure a `package.json` exists in `context.cwd` and an npm auth token is present.\n").
-spec verify_conditions(
version_bump@config:plugin_spec(),
version_bump@context:context()
) -> {ok, nil} | {error, version_bump@error:release_error()}.
verify_conditions(_, Context) ->
gleam@result:'try'(
ensure_package_json_exists(erlang:element(2, Context)),
fun(_) -> case erlang:element(12, Context) of
true ->
{ok, nil};
false ->
ensure_npm_token(Context)
end end
).
-file("src/version_bump/plugins/npm.gleam", 38).
?DOC(
" Build the npm plugin: implements `verify_conditions`, `prepare`, and\n"
" `publish`.\n"
).
-spec plugin() -> version_bump@plugin:plugin().
plugin() ->
_record = version_bump@plugin:new(<<"npm"/utf8>>),
{plugin,
erlang:element(2, _record),
{some, fun verify_conditions/2},
erlang:element(4, _record),
erlang:element(5, _record),
erlang:element(6, _record),
erlang:element(7, _record),
{some, fun prepare/2},
{some, fun publish/2},
erlang:element(10, _record),
erlang:element(11, _record)}.