npm scripts on steroids!
![npm version](https://badge.fury.io/js/makfy.svg)
Install it globally npm install -g makfy
or locally npm install --save-dev makfy
To support this project star it on github!
What is makfy?
makfy is an evolution of npm-scripts to help you automate time-consuming tasks in your development workflow.
Why makfy?
Build systems should be simple, and that's why we guess npm scripts are gaining traction lately.
makfy tries to follow that KISS philosophy while adapting gracefully to complex build requirements.
What does it inherit from npm scripts?
-
It is still shell-based in its core.
-
The gazillion CLI tools available in npm (browserify, rimraf, webpack...) are still available for you to use.
In other words, if there's a CLI version of it available nothing else is needed to use it with makfy.
What does it add?
-
Javascript powered
makfyfile.js
is a javascript file, so you can use it to decide how the shell commands should run or when.
-
Easy argument definition
It is easy to create command line arguments for each command, plus validation is made automatically.
-
Command line help auto-generation
So you don't have to dig into config files to know what each command does.
-
Concurrency made easy
Thanks to async/await plus some nifty tricks (and concurrent logs look good!).
-
Utils included, such as a smart cache!
For example it includes a file checker so shell commands are not run twice if not needed, or only run on the files that changed.
-
Strong validation of makfyfile.js
files
You should not be left wondering if you mistyped something.
-
Colorized detailed logs
So it is easy to spot where your build is currently at and/or where it failed.
Sample makfyfile.js
files
A simple example (run with makfy clean
).
Note: To use the async/await syntax you must install node 7.6+; if you can't then you can either use promises (though the syntax won't be as nice) or babelize/typescript compile the makfyfile.js
file.
module.exports = {
commands: {
clean: {
run: async(exec) => {
await exec(
'rimraf ./dist-a',
'rimraf ./dist-b',
[ 'rimraf ./dist-c', 'rimraf ./dist-d' ]
);
}
}
}
};
Another one but with arguments and help (run with makfy clean --dev
, makfy clean --prod
or makfy clean --dev --prod
).
module.exports = {
commands: {
clean: {
desc: 'clean the project',
args: {
prod: { type: 'flag', desc: 'production clean' },
dev: { type: 'flag', desc: 'dev clean' }
},
run: async(exec, args) => {
await exec(
args.prod ? 'rimraf ./dist-prod' : null,
args.dev ? 'rimraf ./dist-dev' : null
);
}
}
}
};
The help we will get when running makfy --list
.
using command file 'makfyfile.js'...
listing all commands...
clean [--dev] [--prod]
- clean the project
[--dev] dev clean (default: false)
[--prod] production clean (default: false)
Running commands inside commands (makfy build
).
module.exports = {
commands: {
build: {
run: async(exec) => {
await exec(
'@clean',
{ _: 'clean' },
...
);
}
}
clean: {
...
}
}
};
Running commands inside commands and sending them arguments.
module.exports = {
commands: {
build: {
run: async(exec) => {
await exec(
'@clean --dev --prod',
{ _: 'clean', args: { dev: true, prod: true }},
...
);
}
}
clean: {
...
}
}
};
Pro-tip!
Running the typescript compiler, but only if the sources did not change - this reduces build times tremendously!
module.exports = {
commands: {
compile: {
run: async(exec, args, utils) => {
const delta = await utils.getFileChanges('typescript', [
'./src/**/*.ts',
'./src/**/*.tsx'
]);
if (delta.hasChanges) {
await exec(
'tsc -p .'
);
}
}
}
}
};
Documentation
The basic structure of a makfyfile.js
is as follows:
module.exports = {
commands: {
[commandName]: {
run: async(exec, args, utils) => Promise<void>,
desc?: string,
args?: {
[argName]: ArgDefinition
},
internal?: boolean
}
},
options?: Options
};
In more detail:
commands: { [commandName]: Command }
commands
is an object with alphanumeric keys, which are the command names.
Command: { run, desc?, args? }
-
run: async(exec, args, utils) => Promise<void>
An async function that takes three arguments, exec
, args
and utils
.
-
desc?: string
An optional property that defines what the command does so it is shown when using makfy --list
.
-
args?: { [argName]: ArgDefinition }
An optional object of argument definitions that can be passed to that command using makfy commandName ...args
and that will be automatically validated.
An ArgDefinition
can be:
-
Flag option - { type: 'flag' | 'f' }
An optional flag, false by default unless you use --argName
-
String option - { type: 'string' | 's', byDefault?: string }
A string option, required if no byDefault
is given (--argName=string
, use quotes if it has to have spaces)
-
Enum option - { type: 'enum' | 'e', values: string[], byDefault?: string }
An enum option where only values
are valid, required if no byDefault
is given (--argName=string
)
All of them accept a desc?: string
property in case you want to add a given help string to them.
-
internal?: boolean
An optional boolean that indicates that it is an internal command, that is, it should not be shown when listing and cannot be invoked directly from the command line (default: false
).
options: {profile?, showTime?}
options
is an optional object that can be exported to set the default of some options:
-
profile: boolean
When set it will log how much each shell command takes (default: false
)
-
showTime: boolean
When set it will show the current time near each log line (default: false
)
Utility methods (provided by utils
)
-
escape: (...parts: string[]) => string
Escapes all parts of a given shell command.
For example, escape('hello', 'to this world')
will return hello "to this world"
under a cmd shell and hello 'to this world'
under other shells .
-
fixPath: (path: string, style: 'autodetect' | 'windows' | 'posix') => string
Fixes a path so it is valid under a given OS, by swapping /
and \
if needed, plus converting c:\...
to /c/...
in mingw in windows.
The optional style argument forces the result to be valid in windows or posix (default: 'autodetect'
).
-
setEnvVar: (name: string, value: string | undefined) => string
Returns a command string that can be used inside exec
to set/clear an environment variable.
For example, setEnvVar('NODE_ENV', 'development)
will return 'set NODE_ENV=development'
under a cmd shell and 'export NODE_ENV=development'
under other shells.
-
expandGlobs: async (globPatterns: string[]) => Promise<string[]>
Expand one or more glob patterns into and array of single files. Note that if the glob pattern starts with '!!'
then matched files will be removed from previous glob pattern results rather than added.
For example, to match all json files except for package.json files pass [ './**/*.json', '!!./**/package.json' ]
.
-
getFileChangesAsync: async (contextName: string, globPatterns: string[] | string, options: { log = true }) => Promise<GetFileChangesResult>
Returns an object which includes the changes to the given files (given a certain context) since the last successful run.
If there was no previous successful run (or the cache was cleared) then it is considered a clean run.
{
hasChanges: boolean,
cleanRun: boolean,
added: string[],
removed: string[],
modified: string[],
unmodified: string[]
}
Useful for example if you don't want to rerun the babel if none of the sources changed.
The single option avaible is log
to log the cache verification result (default: true
).
Notes:
- If you generate two different targets based on the same source files (for example a production vs a debug bundle) make sure to use different context names for each one.
- This function will create files inside a
.makfy-cache
folder at the end of every successful run. - If you change the
makfyfile.js
contents then a clean run will be assumed. This is done so you don't have to manually clean the cache folder every time you make changes to it.
-
cleanCache: () => void
Cleans the .makfy-cache
folder. Use it if you want to make sure all next calls to getFileChangesAsync
work as if it was a clean run.
FAQ
Recommended CLI packages for cross-platform commands
- Set/unset an environment variable: Just invoke
utils.setEnvVar
inside an exec
call. Alternatively use cross-env. - Delete files/directories: rimraf
- Copy files/directories: ncp
- Create a directory: mkdirp
Keeping the context between exec
executions
Executions inside a very same exec
call keep track of changes to the current working directory and environment variables.
If you wish to keep the context between different exec
executions you can do so like this:
const a = await exec(...);
await a.keepContext(...);
Note: In unix style shells you need to export the variable for it to be tracked (e.g. export NODE_ENV=production
). Consider using utils.setEnvVar
, which does this for you.