Update package.json version with pre-commit hook - node.js

I'm trying to automate the version bump of a Node.js project hosted on a private github repo.
The project is meant to run locally, so it's not published, neither released. People in my organization just pull the main branch and run it on their machines with yarn && yarn start.
What I want to achieve
What I want to achieve is that in the pre-commit phase a version bump is made (major, patch or minor) to the package.json of this project and committed together with the code changed according to the commit message.
All I want to do is that my PR is including the change in the package.json without me having to do it manually. I don't need releases or CI for this.
What I did successfully
I have setup Husky and commitlint in order to validate conventional commit message and it works fine.
What I tried, but failed
I tried to use semantic-release and other packages to provide this functionality, but they all imply there's a CI build or release somewhere so I'm stuck.
Any idea?

in case you have any kind of CICD pipeline (not entirely limited to git hooks scope) you can use gitversion cli
it supports version bump per commit message regex configuration example:
major-version-bump-message: '\+semver:\s?(breaking|major)'
minor-version-bump-message: '\+semver:\s?(feature|minor)'
patch-version-bump-message: '\+semver:\s?(fix|patch)'
no-bump-message: '\+semver:\s?(none|skip)'
means that a commit with message my commit msg +semver: fix will trigger a patch version bump
see this article with detailed explanation for setup.

Instead of pre-commit you should use the commit-msg hook.
The pre-commit hook is triggered before there is a commit message yet, but if you use the commit-msg hook, it will be triggered after there is a commit message.
The hook will trigered your script and the first argument your script will receive will be the commit message itself.
Then you can write any logic on top of the commit message. You can check the message, and update version based of the message as you requested. There are many ways to it
npm version major

you can user updateCommitId modules to write custom node JS code to run during pre-commit hook. Sample working code can look like below. Please upgrade as deemed required.
{
"version": "1.12.0",
"dependencies": {},
"pre-commit": ["updateCommitId"],
"scripts": {
"updateCommitId": "node updateVersion.js"
},
"devDependencies": {
"pre-commit": "^1.2.2",
"semver": "^7.3.5",
"simple-git": "^3.2.6"
}
}
const semver = require("semver");
const simpleGit = require("simple-git");
const options = {
baseDir: process.cwd(),
binary: "git",
maxConcurrentProcesses: 6,
};
const git = simpleGit(options);
const json = require("./package.json");
function savePackage() {
require("fs").writeFileSync(
process.cwd() + "/package.json",
JSON.stringify(json, null, 2)
);
}
async function updateVersion() {
try {
if (json.version) {
const version = semver.parse(json.version);
version.inc("minor");
json.version = version.toString();
savePackage();
await git.add("package.json");
process.exit(0);
}
} catch (e) {
console.log(e);
process.exit(1);
}
}
updateVersion();

Related

Node JS: How to check if a dependent application is installed on target machine?

I would like to implement an external dependency validation logic in my Node JS console application. For example, git. In the terminal "git --version" would respond with current version of the git installed. I might use child_process module in Node and invoke shell commands but is there a better way to do it? It should work regardless of the host operating system.
Why Am I having such requirement?
My application should create a git like merge conflict if 2 or 3 versions (Modified, Original, Remote) of the file having conflicting changes. I wish I could use some node modules to achieve. But it turns that there is none. So I decided to use 'git merge-file'. But before executing the command, I would want to check if git is installed or not. This might seem odd but your suggestions are valuable. Thanks in advance.
Child process is the solution you should go for, as you have already figured it out. It's the only way to interact with other processes from Node.js application.
You can have something like:
const { exec } = require('child_process');
exec('git --version', error => {
if (error) {
// Git doesn't exist
// Add your handler code here
}
else {
// Git exists
// Add remaining of code here
}
});

Building NodeJS using Gradle

I'm very new to Gradle. I started reading about it yesterday. I found an example build.gradle that builds a node application. I'm a little bit confused in the contents of the file. I'm not sure which ones are reserved or predefined words. One of the strings is node. It wasn't used somewhere but I figured out it was needed by the node plugin.
buildscript {
repositories {
mavenCentral()
maven {
url 'https://plugins.gradle.org/m2/'
}
}
dependencies {
classpath 'com.moowork.gradle:gradle-node-plugin:1.2.0'
}
}
apply plugin: 'base'
apply plugin: 'com.moowork.node' // gradle-node-plugin
node {
/* gradle-node-plugin configuration
https://github.com/srs/gradle-node-plugin/blob/master/docs/node.md
Task name pattern:
./gradlew npm_<command> Executes an NPM command.
*/
// Version of node to use.
version = '10.14.1'
// Version of npm to use.
npmVersion = '6.4.1'
// If true, it will download node using above parameters.
// If false, it will try to use globally installed node.
download = true
}
npm_run_build {
// make sure the build task is executed only when appropriate files change
inputs.files fileTree('public')
inputs.files fileTree('src')
// 'node_modules' appeared not reliable for dependency change detection (the task was rerun without changes)
// though 'package.json' and 'package-lock.json' should be enough anyway
inputs.file 'package.json'
inputs.file 'package-lock.json'
outputs.dir 'build'
}
// pack output of the build into JAR file
task packageNpmApp(type: Zip) {
dependsOn npm_run_build
baseName 'npm-app'
extension 'jar'
destinationDir file("${projectDir}/build_packageNpmApp")
from('build') {
// optional path under which output will be visible in Java classpath, e.g. static resources path
into 'static'
}
}
// declare a dedicated scope for publishing the packaged JAR
configurations {
npmResources
}
configurations.default.extendsFrom(configurations.npmResources)
// expose the artifact created by the packaging task
artifacts {
npmResources(packageNpmApp.archivePath) {
builtBy packageNpmApp
type 'jar'
}
}
assemble.dependsOn packageNpmApp
String testsExecutedMarkerName = "${projectDir}/.tests.executed"
task test(type: NpmTask) {
dependsOn assemble
// force Jest test runner to execute tests once and finish the process instead of starting watch mode
environment CI: 'true'
args = ['run', 'test']
inputs.files fileTree('src')
inputs.file 'package.json'
inputs.file 'package-lock.json'
// allows easy triggering re-tests
doLast {
new File(testsExecutedMarkerName).text = 'delete this file to force re-execution JavaScript tests'
}
outputs.file testsExecutedMarkerName
}
check.dependsOn test
clean {
delete packageNpmApp.archivePath
delete testsExecutedMarkerName
}
Also, how is the build.gradle parsed? I'm also wondering how it is able to magically download node and npm tools.
This is a very general synopsis:
Gradle aims to hide away the logic from developers.
Most *.gradle files contain configuration blocks (closures) to specify HOW logic should run.
Plugins augment gradle with more configurable logic.
Also, 'convention over configuration' is a practice emphasize in gradle and its plugins, providing sensible defaults to minimize developers' configuration efforts.
The com.moowork.node plugin is configured through the node extension block.
Extension blocks are gradle's way to allow plugins to add more 'reserved' words to the standard gradle model.
The download = true configuration tells the plugin to download node (version = '10.14.1') and nmp (npmVersion = '6.4.1') in your project's root (unless you override its defaults as well).
The download of these tools will occur when any of the plugin's task is invoked.
Hope this helps.
In your snippet only true is keyword, the other things are methods or getters coming from Gradle or Node JS plugin:
apply plugin: ... is a method from org.gradle.api.Project.apply(java.util.Map<String, ?>)
node is a method autogenerated by Gradle with signature void node(Closure<com.moowork.gradle.node.NodeExtension>) (method accepting a code block), see https://github.com/srs/gradle-node-plugin/blob/master/src/main/groovy/com/moowork/gradle/node/NodeExtension.groovy
node { version = ... } - version, npmVersion are fields from NodeExtension class
other things similarly, everything is a method or a field. If you're using IntelliJ, use Ctrl+mouse click to navigate to the originating method/field declaration.

How to create custom husky hook? (node.js/package.json)

How to create custom husky hook?
I would like to do something like this:
// package.json
...
husky: {
"pre-commit": "node customHook.js"
},
...
How to get access to the commit params from the customHook.js file?
P.S. I found almost the same question, but unfortunately it doesn't work for me.
I found a solution.
Change "pre-commit" hook to "commit-msg" in the package.json file. After, you are able to get commit message by using the next line of code:
// terminal (cmd)
git commit -m "my commit message"
// customHook.js file
const message = require('fs').readFileSync(process.env.HUSKY_GIT_PARAMS, 'utf-8');
console.log(message); // "my commit message"

Prevent `npm publish` when ran directly

I am not sure weather it is possible or not.
Is it possible to prevent publish when npm publish ran directly and make it accessible only via scripts.
User must be denied when npm publish is executed directly. i.e. User mush be able to publish via any scripts or npm run <script>
or
is there a way to tell npm only to publish <folder>/ or to look for a tarball when published.
If I mark it private I won't be able to publish at all. My main intention was to prevent accidental publishes.
NPM team gave a simple workaround which is awsome.
package.json
{
"prepublishOnly": "node prepublish.js",
"release": "RELEASE_MODE=true npm publish"
}
prepublish.js
const RELEASE_MODE = !!(process.env.RELEASE_MODE)
if (!RELEASE_MODE) {
console.log('Run `npm run release` to publish the package')
process.exit(1) //which terminates the publish process
}
Mark the package as private:
If you set "private": true in your package.json, then npm will refuse
to publish it.
This is a way to prevent accidental publication of private
repositories. If you would like to ensure that a given package is only
ever published to a specific registry (for example, an internal
registry), then use the publishConfig dictionary described below to
override the registry config param at publish-time.
{
"name": "some",
"version": "1.0.0",
"private": true
}
If you are trying to force something to happen before publishing, leverage the prepublish or prepublishOnly npm-script.
Yes, we can restrict npm to prevent accidental publish by making private: true in package.json
You can have script for publish also
In your package.json
{
"scripts": {
"publish:mypackages": "npm publish folder1/file1.tgz --registry http://custom-registry..."
}
}
Now in cmd: npm run publish:mypackages
It publishes the given tarball to the registry you have given.

Add git information to create-react-app

In development, I want to be able to see the build information (git commit hash, author, last commit message, etc) from the web. I have tried:
use child_process to execute a git command line, and read the result (Does not work because browser environment)
generate a buildInfo.txt file during npm build and read from the file (Does not work because fs is also unavailable in browser environment)
use external libraries such as "git-rev"
The only thing left to do seems to be doing npm run eject and applying https://www.npmjs.com/package/git-revision-webpack-plugin , but I really don't want to eject out of create-react-app. Anyone got any ideas?
On a slight tangent (no need to eject and works in develop),
this may be of help to other folk looking to add their current git commit SHA into their index.html as a meta-tag by adding:
REACT_APP_GIT_SHA=`git rev-parse --short HEAD`
to the build script in the package.json and then adding (note it MUST start with REACT_APP... or it will be ignored):
<meta name="ui-version" content="%REACT_APP_GIT_SHA%">
into the index.html in the public folder.
Within react components, do it like this:
<Component>{process.env.REACT_APP_GIT_SHA}</Component>
I created another option inspired by Yifei Xu's response that utilizes es6 modules with create-react-app. This option creates a javascript file and imports it as a constant inside of the build files. While having it as a text file makes it easy to update, this option ensures it is a js file packaged into the javascript bundle. The name of this file is _git_commit.js
package.json scripts:
"git-info": "echo export default \"{\\\"logMessage\\\": \\\"$(git log -1 --oneline)\\\"}\" > src/_git_commit.js",
"precommit": "lint-staged",
"start": "yarn git-info; react-scripts start",
"build": "yarn git-info; react-scripts build",
A sample component that consumes this commit message:
import React from 'react';
/**
* This is the commit message of the last commit before building or running this project
* #see ./package.json git-info for how to generate this commit
*/
import GitCommit from './_git_commit';
const VersionComponent = () => (
<div>
<h1>Git Log: {GitCommit.logMessage}</h1>
</div>
);
export default VersionComponent;
Note that this will automatically put your commit message in the javascript bundle, so do ensure no secure information is ever entered into the commit message. I also add the created _git_commit.js to .gitignore so it's not checked in (and creates a crazy git commit loop).
It was impossible to be able to do this without ejecting until Create React App 2.0 (Release Notes) which brought with it automatic configuration of Babel Plugin Macros which run at compile time. To make the job simpler for everyone, I wrote one of those macros and published an NPM package that you can import to get git information into your React pages: https://www.npmjs.com/package/react-git-info
With it, you can do it like this:
import GitInfo from 'react-git-info/macro';
const gitInfo = GitInfo();
...
render() {
return (
<p>{gitInfo.commit.hash}</p>
);
}
The project README has some more information. You can also see a live demo of the package working here.
So, turns out there is no way to achieve this without ejecting, so the workaround I used is:
1) in package.json, define a script "git-info": "git log -1 --oneline > src/static/gitInfo.txt"
2) add npm run git-info for both start and build
3) In the config js file (or whenever you need the git info), i have
const data = require('static/gitInfo.txt')
fetch(data).then(result => {
return result.text()
})
My approach is slightly different from #uidevthing's answer. I don't want to pollute package.json file with environment variable settings.
You simply have to run another script that save those environment variables into .env file at the project root. That's it.
In the example below, I'll use typescript but it should be trivial to convert to javascript anyway.
package.json
If you use javascript it's node scripts/start.js
...
"start": "ts-node scripts/start.ts && react-scripts start",
scripts/start.ts
Create a new script file scripts/start.ts
const childProcess = require("child_process");
const fs = require("fs");
function writeToEnv(key: string = "", value: string = "") {
const empty = key === "" && value === "";
if (empty) {
fs.writeFile(".env", "", () => {});
} else {
fs.appendFile(".env", `${key}='${value.trim()}'\n`, (err) => {
if (err) console.log(err);
});
}
}
// reset .env file
writeToEnv();
childProcess.exec("git rev-parse --abbrev-ref HEAD", (err, stdout) => {
writeToEnv("REACT_APP_GIT_BRANCH", stdout);
});
childProcess.exec("git rev-parse --short HEAD", (err, stdout) => {
writeToEnv("REACT_APP_GIT_SHA", stdout);
});
// trick typescript to think it's a module
// https://stackoverflow.com/a/56577324/9449426
export {};
The code above will setup environment variables and save them to .env file at the root folder. They must start with REACT_APP_. React script then automatically reads .env at build time and then defines them in process.env.
App.tsx
...
console.log('REACT_APP_GIT_BRANCH', process.env.REACT_APP_GIT_BRANCH)
console.log('REACT_APP_GIT_SHA', process.env.REACT_APP_GIT_SHA)
Result
REACT_APP_GIT_BRANCH master
REACT_APP_GIT_SHA 042bbc6
More references:
https://create-react-app.dev/docs/adding-custom-environment-variables/#adding-development-environment-variables-in-env
If your package.json scripts are always executed in a unix environment you can achieve the same as in #NearHuscarl answer, but with fewer lines of code by initializing your .env dotenv file from a shell script. The generated .env is then picked up by the react-scripts in the subsequent step.
"scripts": {
"start": "sh ./env.sh && react-scripts start"
"build": "sh ./env.sh && react-scripts build",
}
where .env.sh is placed in your project root and contains code similar to the one below to override you .env file content on each build or start.
{
echo BROWSER=none
echo REACT_APP_FOO=bar
echo REACT_APP_VERSION=$(git rev-parse --short HEAD)
echo REACT_APP_APP_BUILD_DATE=$(date)
# ...
} > .env
Since the .env is overridden on each build, you may consider putting it on the .gitignore list to avoid too much noise in your commit diffs.
Again the disclaimer: This solution only works for environments where a bourne shell interpreter or similar exists, i.e. unix.
You can easily inject your git information like commit hash into your index.html using CRACO and craco-interpolate-html-plugin. Such way you won't have to use yarn eject and it also works for development server environment along with production builds.
After installing CRACO the following config in craco.config.js worked for me:
const interpolateHtml = require('craco-interpolate-html-plugin');
module.exports = {
plugins: [
{
plugin: interpolateHtml,
// Enter the variable to be interpolated in the html file
options: {
BUILD_VERSION: require('child_process')
.execSync('git rev-parse HEAD', { cwd: __dirname })
.toString().trim(),
},
},
],
};
and in your index.html:
<meta name="build-version" content="%BUILD_VERSION%" />
Here are the lines of code to add in package.json to make it all work:
"scripts": {
"start": "craco start",
"build": "craco build"
}

Resources