cache-cmd 0.2.0-dev.4a662091d40f9510cff02e70023e5919708daa8f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Dany Castillo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # cache-cmd
2
+
3
+ Cache a command based on
4
+
5
+ - time since last run
6
+ - file change
7
+
8
+ ## Install
9
+
10
+ ```sh
11
+ yarn add --dev cache-cmd
12
+ ```
13
+
14
+ or
15
+
16
+ ```sh
17
+ npm install --save-dev cache-cmd
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```sh
23
+ # Shows help
24
+ yarn cache-cmd --help
25
+
26
+ # Runs command if it was not run in the last 20s
27
+ yarn cache-cmd "echo ran this command" --time 20s
28
+
29
+ # Runs comand if yarn.lock in current directory changed since last run
30
+ yarn cache-cmd "yarn install" --file yarn.lock
31
+
32
+ # Additionally uses custom cache directory instead of default in node_modules
33
+ yarn cache-cmd "yarn install" --file yarn.lock --cache-dir .config/cache
34
+
35
+ # Runs command if it was not run in a month or any of the files changed
36
+ yarn cache-cmd "yarn install" --time 1mo --file yarn.lock --file package.json
37
+
38
+ # Shows path to cache directory
39
+ yarn cache-cmd cache dir
40
+
41
+ # Clear cache
42
+ yarn cache-cmd cache clear
43
+ ```
44
+
45
+ You can use it to execute commands conditionally in `package.json` scripts.
46
+
47
+ ```json
48
+ {
49
+ "scripts": {
50
+ "dev": "cache-cmd \"yarn\" --file yarn.lock && start-dev-server"
51
+ }
52
+ }
53
+ ```
54
+
55
+ ## Contribute
56
+
57
+ If you find a bug or something you don't like, please [submit an issue](https://github.com/dcastil/cache-cmd/issues/new) or a pull request. I'm happy about any kind of feedback!
package/dist/cli.cjs ADDED
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ var hardRejection = require('hard-rejection');
5
+ var sade = require('sade');
6
+ var del = require('del');
7
+ var path = require('path');
8
+ var findCacheDir = require('find-cache-dir');
9
+ var makeDir = require('make-dir');
10
+ var dateFns = require('date-fns');
11
+ var execSh = require('exec-sh');
12
+ var flatCache = require('flat-cache');
13
+ var lodash = require('lodash');
14
+ var hasha = require('hasha');
15
+
16
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
17
+
18
+ var hardRejection__default = /*#__PURE__*/_interopDefaultLegacy(hardRejection);
19
+ var sade__default = /*#__PURE__*/_interopDefaultLegacy(sade);
20
+ var del__default = /*#__PURE__*/_interopDefaultLegacy(del);
21
+ var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
22
+ var findCacheDir__default = /*#__PURE__*/_interopDefaultLegacy(findCacheDir);
23
+ var makeDir__default = /*#__PURE__*/_interopDefaultLegacy(makeDir);
24
+ var execSh__default = /*#__PURE__*/_interopDefaultLegacy(execSh);
25
+ var flatCache__default = /*#__PURE__*/_interopDefaultLegacy(flatCache);
26
+ var hasha__default = /*#__PURE__*/_interopDefaultLegacy(hasha);
27
+
28
+ var name = "cache-cmd";
29
+ var version = "0.2.0";
30
+
31
+ function getCacheDirectoryPath(relativeCacheDirectoryPath) {
32
+ if (relativeCacheDirectoryPath) {
33
+ return path__default["default"].resolve(process.cwd(), relativeCacheDirectoryPath);
34
+ }
35
+
36
+ const resolvedCacheDirectory = findCacheDir__default["default"]({
37
+ name
38
+ });
39
+
40
+ if (!resolvedCacheDirectory) {
41
+ throw Error('Could not find cache directory. Please provide a cache directory manually.');
42
+ }
43
+
44
+ return resolvedCacheDirectory;
45
+ }
46
+ function createCache(relativeCacheDirectoryPath) {
47
+ if (relativeCacheDirectoryPath) {
48
+ const absoluteCacheDirectoryPath = makeDir__default["default"].sync(relativeCacheDirectoryPath);
49
+ return absoluteCacheDirectoryPath;
50
+ }
51
+
52
+ const resolvedCachePath = findCacheDir__default["default"]({
53
+ name,
54
+ create: true
55
+ });
56
+
57
+ if (!resolvedCachePath) {
58
+ throw Error('Could not find cache directory. Please provide a cache directory manually.');
59
+ }
60
+
61
+ return resolvedCachePath;
62
+ }
63
+
64
+ function clearCacheDirectory(relativeCacheDirectory) {
65
+ const deletedPaths = del__default["default"].sync(getCacheDirectoryPath(relativeCacheDirectory));
66
+
67
+ if (deletedPaths.length === 0) {
68
+ console.log('No cache to clear');
69
+ } else if (deletedPaths.length === 1) {
70
+ console.log(`Deleted: ${deletedPaths[0]}`);
71
+ } else {
72
+ console.log('Deleted:\n', deletedPaths.join('\n'));
73
+ }
74
+ }
75
+
76
+ function showCacheDirectory(relativeCacheDirectory) {
77
+ console.log(getCacheDirectoryPath(relativeCacheDirectory));
78
+ }
79
+
80
+ function getCacheKey({
81
+ duration,
82
+ filePaths,
83
+ command
84
+ }) {
85
+ return ['cwd:' + process.cwd(), 'cmd:' + command, duration && 'time:' + JSON.stringify(duration), filePaths.length !== 0 && 'files:' + filePaths.join(',')].filter(Boolean).join(' ---');
86
+ }
87
+
88
+ function getDuration(durationString) {
89
+ if (!/^(\d+[a-z]+)+$/gi.test(durationString)) {
90
+ throw Error(`Invalid duration: ${durationString}`);
91
+ }
92
+
93
+ const duration = {
94
+ years: undefined,
95
+ months: undefined,
96
+ weeks: undefined,
97
+ days: undefined,
98
+ hours: undefined,
99
+ minutes: undefined,
100
+ seconds: undefined
101
+ };
102
+ durationString.match(/\d+[a-z]+/gi).forEach(durationPart => {
103
+ const [, stringQuantity, unitShort] = /(\d+)([a-z]+)/gi.exec(durationPart);
104
+ const quantity = Number(stringQuantity);
105
+ const unitLong = {
106
+ y: 'years',
107
+ mo: 'months',
108
+ w: 'weeks',
109
+ d: 'days',
110
+ h: 'hours',
111
+ m: 'minutes',
112
+ s: 'seconds'
113
+ }[unitShort];
114
+
115
+ if (Number.isNaN(quantity) || !unitLong) {
116
+ throw Error(`Invalid duration part: ${durationPart}`);
117
+ }
118
+
119
+ if (duration[unitLong] !== undefined) {
120
+ throw Error(`Duration with unit ${unitLong} was supplied multiple times`);
121
+ }
122
+
123
+ duration[unitLong] = quantity;
124
+ });
125
+ return duration;
126
+ }
127
+
128
+ async function getFileHashes(filePaths) {
129
+ return Object.fromEntries(await Promise.all(filePaths.map(filePath => hasha__default["default"].fromFile(filePath, {
130
+ algorithm: 'md5'
131
+ }).then(hash => [filePath, hash]))));
132
+ }
133
+
134
+ function getFilePaths(relativeFilePaths) {
135
+ const cwd = process.cwd();
136
+ return relativeFilePaths.map(file => path__default["default"].resolve(cwd, file)).sort();
137
+ }
138
+
139
+ async function runCommand({
140
+ relativeCacheDirectory,
141
+ command,
142
+ cacheByTime: cacheByTime,
143
+ cacheByFiles: cacheByFiles,
144
+ shouldCacheOnError
145
+ }) {
146
+ if (!cacheByTime && cacheByFiles.length === 0) {
147
+ await execSh__default["default"].promise(command);
148
+ return;
149
+ }
150
+
151
+ const cache = flatCache__default["default"].load('commands-cache.json', createCache(relativeCacheDirectory));
152
+ const filePaths = getFilePaths(cacheByFiles);
153
+ const duration = cacheByTime ? getDuration(cacheByTime) : undefined;
154
+ const cacheKey = getCacheKey({
155
+ duration,
156
+ filePaths,
157
+ command
158
+ });
159
+ const cacheData = cache.getKey(cacheKey);
160
+ const fileHashes = filePaths.length === 0 ? undefined : await getFileHashes(filePaths);
161
+ const currentDate = new Date();
162
+ const areFileHashesEqual = lodash.isEqual(cacheData == null ? void 0 : cacheData.fileHashes, fileHashes);
163
+
164
+ const isWithinCacheTime = (() => {
165
+ if (!duration) {
166
+ return true;
167
+ }
168
+
169
+ const lastRun = cacheData == null ? void 0 : cacheData.lastRun;
170
+
171
+ if (!lastRun) {
172
+ return false;
173
+ }
174
+
175
+ return dateFns.isAfter(dateFns.add(new Date(lastRun), duration), currentDate);
176
+ })();
177
+
178
+ if (areFileHashesEqual && isWithinCacheTime) {
179
+ return;
180
+ }
181
+
182
+ let execPromise = execSh__default["default"].promise(command);
183
+
184
+ if (shouldCacheOnError) {
185
+ execPromise = execPromise.catch(error => {
186
+ cache.setKey(cacheKey, {
187
+ lastRun: currentDate,
188
+ fileHashes
189
+ });
190
+ cache.save(true);
191
+ throw error;
192
+ });
193
+ }
194
+
195
+ await execPromise;
196
+ cache.setKey(cacheKey, {
197
+ lastRun: currentDate,
198
+ fileHashes
199
+ });
200
+ cache.save(true);
201
+ }
202
+
203
+ hardRejection__default["default"]();
204
+ const program = sade__default["default"]('cache-cmd');
205
+ program.version(version).describe('Run and cache a command based on various factors').option('-c, --cache-dir', 'Cache directory to use (default: .cache/cache-cmd in nearest node_modules)');
206
+ program.command('run <command>', 'Run cached command (if no <command> provided, this is the default)', {
207
+ default: true
208
+ }).option('-f, --file', 'Run command only when file content changes').option('-t, --time', 'Run command only after specified time (unit with s,m,h,d,w,mo,y)').option('--cache-on-error', 'Cache command run even when command exits with non-zero exit code').example('run "echo ran this command" --time 20s').example('run "./may-fail" --time 20s --cache-on-error').example('run "yarn install" --file yarn.lock').example('run "yarn install" --file yarn.lock --cache-dir .config/cache').example('run "yarn install" --time 1mo --file yarn.lock --file package.json').action((command, options) => {
209
+ const cacheDirectory = options['cache-dir'];
210
+ const time = options.time;
211
+ const file = options.file;
212
+ const shouldCacheOnError = options['cache-on-error'];
213
+ const files = file === undefined ? [] : Array.isArray(file) ? file : [file];
214
+
215
+ if (typeof command !== 'string') {
216
+ throw Error('Invalid <command> supplied');
217
+ }
218
+
219
+ if (cacheDirectory !== undefined && typeof cacheDirectory !== 'string') {
220
+ throw Error('Invalid --cache-dir supplied');
221
+ }
222
+
223
+ if (time !== undefined && typeof time !== 'string') {
224
+ throw Error('Invalid --time supplied');
225
+ }
226
+
227
+ if (files.some(file => typeof file !== 'string')) {
228
+ throw Error('Invalid --file supplied');
229
+ }
230
+
231
+ if (shouldCacheOnError !== undefined && typeof shouldCacheOnError !== 'boolean') {
232
+ throw Error('Invalid --cache-on-error supplied');
233
+ }
234
+
235
+ runCommand({
236
+ relativeCacheDirectory: cacheDirectory,
237
+ command,
238
+ cacheByTime: time,
239
+ cacheByFiles: files,
240
+ shouldCacheOnError
241
+ });
242
+ });
243
+ program.command('cache dir', 'Show cache directory path used by cache-cmd').action(options => {
244
+ const cacheDirectory = options['cache-dir'];
245
+
246
+ if (cacheDirectory !== undefined && typeof cacheDirectory !== 'string') {
247
+ throw Error('Invalid --cache-dir supplied');
248
+ }
249
+
250
+ showCacheDirectory(cacheDirectory);
251
+ });
252
+ program.command('cache clear', 'Clear cache used by cache-cmd').action(options => {
253
+ const cacheDirectory = options['cache-dir'];
254
+
255
+ if (cacheDirectory !== undefined && typeof cacheDirectory !== 'string') {
256
+ throw Error('Invalid --cache-dir supplied');
257
+ }
258
+
259
+ clearCacheDirectory(cacheDirectory);
260
+ });
261
+ program.parse(process.argv);
262
+ //# sourceMappingURL=cli.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.cjs","sources":["../src/utils/get-cache-dir.ts","../src/cache-clear.ts","../src/cache-dir.ts","../src/utils/get-cache-key.ts","../src/utils/get-duration.ts","../src/utils/get-file-hashes.ts","../src/utils/get-file-paths.ts","../src/run.ts","../src/cli.ts"],"sourcesContent":["import path from 'path'\n\nimport findCacheDir from 'find-cache-dir'\nimport makeDir from 'make-dir'\n\nimport { name } from '../../package.json'\n\nexport function getCacheDirectoryPath(relativeCacheDirectoryPath: string | undefined) {\n if (relativeCacheDirectoryPath) {\n return path.resolve(process.cwd(), relativeCacheDirectoryPath)\n }\n\n const resolvedCacheDirectory = findCacheDir({ name })\n\n if (!resolvedCacheDirectory) {\n throw Error('Could not find cache directory. Please provide a cache directory manually.')\n }\n\n return resolvedCacheDirectory\n}\n\nexport function createCache(relativeCacheDirectoryPath: string | undefined) {\n if (relativeCacheDirectoryPath) {\n const absoluteCacheDirectoryPath = makeDir.sync(relativeCacheDirectoryPath)\n\n return absoluteCacheDirectoryPath\n }\n\n const resolvedCachePath = findCacheDir({ name, create: true })\n\n if (!resolvedCachePath) {\n throw Error('Could not find cache directory. Please provide a cache directory manually.')\n }\n\n return resolvedCachePath\n}\n","import del from 'del'\n\nimport { getCacheDirectoryPath } from './utils/get-cache-dir'\n\nexport function clearCacheDirectory(relativeCacheDirectory: string | undefined) {\n const deletedPaths = del.sync(getCacheDirectoryPath(relativeCacheDirectory))\n\n if (deletedPaths.length === 0) {\n console.log('No cache to clear')\n } else if (deletedPaths.length === 1) {\n console.log(`Deleted: ${deletedPaths[0]}`)\n } else {\n console.log('Deleted:\\n', deletedPaths.join('\\n'))\n }\n}\n","import { getCacheDirectoryPath } from './utils/get-cache-dir'\n\nexport function showCacheDirectory(relativeCacheDirectory: string | undefined) {\n console.log(getCacheDirectoryPath(relativeCacheDirectory))\n}\n","interface GetCacheKeyProps {\n duration: Duration | undefined\n filePaths: string[]\n command: string\n}\n\ninterface Duration {\n years: number | undefined\n months: number | undefined\n weeks: number | undefined\n days: number | undefined\n hours: number | undefined\n minutes: number | undefined\n seconds: number | undefined\n}\n\nexport function getCacheKey({ duration, filePaths, command }: GetCacheKeyProps) {\n return [\n 'cwd:' + process.cwd(),\n 'cmd:' + command,\n duration && 'time:' + JSON.stringify(duration),\n filePaths.length !== 0 && 'files:' + filePaths.join(','),\n ]\n .filter(Boolean)\n .join(' ---')\n}\n","export function getDuration(durationString: string) {\n if (!/^(\\d+[a-z]+)+$/gi.test(durationString)) {\n throw Error(`Invalid duration: ${durationString}`)\n }\n\n const duration = {\n years: undefined as undefined | number,\n months: undefined as undefined | number,\n weeks: undefined as undefined | number,\n days: undefined as undefined | number,\n hours: undefined as undefined | number,\n minutes: undefined as undefined | number,\n seconds: undefined as undefined | number,\n }\n\n durationString.match(/\\d+[a-z]+/gi)!.forEach((durationPart) => {\n const [, stringQuantity, unitShort] = /(\\d+)([a-z]+)/gi.exec(durationPart)!\n const quantity = Number(stringQuantity)\n\n const unitLong = (\n {\n y: 'years',\n mo: 'months',\n w: 'weeks',\n d: 'days',\n h: 'hours',\n m: 'minutes',\n s: 'seconds',\n } as const\n )[unitShort!]\n\n if (Number.isNaN(quantity) || !unitLong) {\n throw Error(`Invalid duration part: ${durationPart}`)\n }\n\n if (duration[unitLong] !== undefined) {\n throw Error(`Duration with unit ${unitLong} was supplied multiple times`)\n }\n\n duration[unitLong] = quantity\n })\n\n return duration\n}\n","import hasha from 'hasha'\n\nexport async function getFileHashes(filePaths: string[]) {\n return Object.fromEntries(\n await Promise.all(\n filePaths.map((filePath) =>\n hasha\n .fromFile(filePath, { algorithm: 'md5' })\n .then((hash) => [filePath, hash] as const)\n )\n )\n )\n}\n","import path from 'path'\n\nexport function getFilePaths(relativeFilePaths: string[]) {\n const cwd = process.cwd()\n\n return relativeFilePaths.map((file) => path.resolve(cwd, file)).sort()\n}\n","import { add, isAfter } from 'date-fns'\nimport execSh from 'exec-sh'\nimport flatCache from 'flat-cache'\nimport { isEqual } from 'lodash'\n\nimport { createCache } from './utils/get-cache-dir'\nimport { getCacheKey } from './utils/get-cache-key'\nimport { getDuration } from './utils/get-duration'\nimport { getFileHashes } from './utils/get-file-hashes'\nimport { getFilePaths } from './utils/get-file-paths'\n\ninterface RunCommandProps {\n relativeCacheDirectory: string | undefined\n command: string\n cacheByTime: string | undefined\n cacheByFiles: string[]\n shouldCacheOnError: boolean | undefined\n}\n\nexport async function runCommand({\n relativeCacheDirectory,\n command,\n cacheByTime: cacheByTime,\n cacheByFiles: cacheByFiles,\n shouldCacheOnError,\n}: RunCommandProps) {\n if (!cacheByTime && cacheByFiles.length === 0) {\n await execSh.promise(command)\n return\n }\n\n const cache = flatCache.load('commands-cache.json', createCache(relativeCacheDirectory))\n const filePaths = getFilePaths(cacheByFiles)\n const duration = cacheByTime ? getDuration(cacheByTime) : undefined\n\n const cacheKey = getCacheKey({ duration, filePaths, command })\n\n const cacheData: unknown = cache.getKey(cacheKey)\n\n const fileHashes = filePaths.length === 0 ? undefined : await getFileHashes(filePaths)\n const currentDate = new Date()\n\n const areFileHashesEqual = isEqual((cacheData as any)?.fileHashes, fileHashes)\n const isWithinCacheTime = (() => {\n if (!duration) {\n return true\n }\n\n const lastRun = (cacheData as any)?.lastRun\n\n if (!lastRun) {\n return false\n }\n\n return isAfter(add(new Date(lastRun), duration), currentDate)\n })()\n\n if (areFileHashesEqual && isWithinCacheTime) {\n return\n }\n\n let execPromise = execSh.promise(command)\n\n if (shouldCacheOnError) {\n execPromise = execPromise.catch((error: unknown) => {\n cache.setKey(cacheKey, {\n lastRun: currentDate,\n fileHashes,\n })\n cache.save(true)\n\n throw error\n })\n }\n\n await execPromise\n\n cache.setKey(cacheKey, {\n lastRun: currentDate,\n fileHashes,\n })\n cache.save(true)\n}\n","#!/usr/bin/env node\n\nimport hardRejection from 'hard-rejection'\nimport sade from 'sade'\n\nimport { version } from '../package.json'\n\nimport { clearCacheDirectory } from './cache-clear'\nimport { showCacheDirectory } from './cache-dir'\nimport { runCommand } from './run'\n\nhardRejection()\n\nconst program = sade('cache-cmd')\n\nprogram\n .version(version)\n .describe('Run and cache a command based on various factors')\n .option(\n '-c, --cache-dir',\n 'Cache directory to use (default: .cache/cache-cmd in nearest node_modules)'\n )\n\nprogram\n .command(\n 'run <command>',\n 'Run cached command (if no <command> provided, this is the default)',\n { default: true }\n )\n .option('-f, --file', 'Run command only when file content changes')\n .option('-t, --time', 'Run command only after specified time (unit with s,m,h,d,w,mo,y)')\n .option('--cache-on-error', 'Cache command run even when command exits with non-zero exit code')\n .example('run \"echo ran this command\" --time 20s')\n .example('run \"./may-fail\" --time 20s --cache-on-error')\n .example('run \"yarn install\" --file yarn.lock')\n .example('run \"yarn install\" --file yarn.lock --cache-dir .config/cache')\n .example('run \"yarn install\" --time 1mo --file yarn.lock --file package.json')\n .action((command: unknown, options: Record<string, unknown>) => {\n const cacheDirectory = options['cache-dir']\n const time = options.time\n const file = options.file\n const shouldCacheOnError = options['cache-on-error']\n const files: unknown[] = file === undefined ? [] : Array.isArray(file) ? file : [file]\n\n if (typeof command !== 'string') {\n throw Error('Invalid <command> supplied')\n }\n\n if (cacheDirectory !== undefined && typeof cacheDirectory !== 'string') {\n throw Error('Invalid --cache-dir supplied')\n }\n\n if (time !== undefined && typeof time !== 'string') {\n throw Error('Invalid --time supplied')\n }\n\n if (files.some((file) => typeof file !== 'string')) {\n throw Error('Invalid --file supplied')\n }\n\n if (shouldCacheOnError !== undefined && typeof shouldCacheOnError !== 'boolean') {\n throw Error('Invalid --cache-on-error supplied')\n }\n\n runCommand({\n relativeCacheDirectory: cacheDirectory,\n command,\n cacheByTime: time,\n cacheByFiles: files as string[],\n shouldCacheOnError,\n })\n })\n\nprogram\n .command('cache dir', 'Show cache directory path used by cache-cmd')\n .action((options: Record<string, unknown>) => {\n const cacheDirectory = options['cache-dir']\n\n if (cacheDirectory !== undefined && typeof cacheDirectory !== 'string') {\n throw Error('Invalid --cache-dir supplied')\n }\n\n showCacheDirectory(cacheDirectory)\n })\n\nprogram\n .command('cache clear', 'Clear cache used by cache-cmd')\n .action((options: Record<string, unknown>) => {\n const cacheDirectory = options['cache-dir']\n\n if (cacheDirectory !== undefined && typeof cacheDirectory !== 'string') {\n throw Error('Invalid --cache-dir supplied')\n }\n\n clearCacheDirectory(cacheDirectory)\n })\n\nprogram.parse(process.argv)\n"],"names":["getCacheDirectoryPath","relativeCacheDirectoryPath","path","resolve","process","cwd","resolvedCacheDirectory","findCacheDir","name","Error","createCache","absoluteCacheDirectoryPath","makeDir","sync","resolvedCachePath","create","clearCacheDirectory","relativeCacheDirectory","deletedPaths","del","length","console","log","join","showCacheDirectory","getCacheKey","duration","filePaths","command","JSON","stringify","filter","Boolean","getDuration","durationString","test","years","undefined","months","weeks","days","hours","minutes","seconds","match","forEach","durationPart","stringQuantity","unitShort","exec","quantity","Number","unitLong","y","mo","w","d","h","m","s","isNaN","getFileHashes","Object","fromEntries","Promise","all","map","filePath","hasha","fromFile","algorithm","then","hash","getFilePaths","relativeFilePaths","file","sort","runCommand","cacheByTime","cacheByFiles","shouldCacheOnError","execSh","promise","cache","flatCache","load","cacheKey","cacheData","getKey","fileHashes","currentDate","Date","areFileHashesEqual","isEqual","isWithinCacheTime","lastRun","isAfter","add","execPromise","catch","error","setKey","save","hardRejection","program","sade","version","describe","option","default","example","action","options","cacheDirectory","time","files","Array","isArray","some","parse","argv"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "cache-cmd",
3
+ "version": "0.2.0-dev.4a662091d40f9510cff02e70023e5919708daa8f",
4
+ "description": "Cache a command based on various factors",
5
+ "keywords": [
6
+ "cache",
7
+ "debounce",
8
+ "cli",
9
+ "conditional",
10
+ "run",
11
+ "cmd",
12
+ "command",
13
+ "time",
14
+ "hash"
15
+ ],
16
+ "homepage": "https://github.com/dcastil/cache-cmd",
17
+ "bugs": {
18
+ "url": "https://github.com/dcastil/cache-cmd/issues"
19
+ },
20
+ "license": "MIT",
21
+ "author": "Dany Castillo",
22
+ "files": [
23
+ "dist",
24
+ "src"
25
+ ],
26
+ "bin": "dist/cli.cjs",
27
+ "type": "module",
28
+ "source": "src/cli.ts",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/dcastil/cache-cmd.git"
32
+ },
33
+ "scripts": {
34
+ "cache-cmd": "node ./dist/cli.cjs",
35
+ "build": "rm -rf dist/* && microbundle --strict --target node --output dist/cli.ts --format cjs --generateTypes false",
36
+ "type-check": "tsc --build",
37
+ "preversion": "if [ -n \"$DANYS_MACHINE\" ]; then git checkout main && git pull; fi",
38
+ "postversion": "if [ -n \"$DANYS_MACHINE\" ]; then git push --follow-tags && open https://github.com/dcastil/cache-cmd/releases; fi"
39
+ },
40
+ "dependencies": {
41
+ "date-fns": "^2.25.0",
42
+ "del": "^6.0.0",
43
+ "exec-sh": "^0.4.0",
44
+ "find-cache-dir": "^3.3.2",
45
+ "flat-cache": "^3.0.4",
46
+ "hard-rejection": "^2.1.0",
47
+ "hasha": "^5.2.2",
48
+ "lodash": "^4.17.21",
49
+ "make-dir": "^3.1.0",
50
+ "sade": "^1.7.4"
51
+ },
52
+ "devDependencies": {
53
+ "@types/find-cache-dir": "^3.2.1",
54
+ "@types/flat-cache": "^2.0.0",
55
+ "@types/lodash": "^4.14.175",
56
+ "@types/sade": "^1.7.3",
57
+ "microbundle": "^0.14.1",
58
+ "prettier": "^2.4.1",
59
+ "typescript": "^4.4.4"
60
+ }
61
+ }
@@ -0,0 +1,15 @@
1
+ import del from 'del'
2
+
3
+ import { getCacheDirectoryPath } from './utils/get-cache-dir'
4
+
5
+ export function clearCacheDirectory(relativeCacheDirectory: string | undefined) {
6
+ const deletedPaths = del.sync(getCacheDirectoryPath(relativeCacheDirectory))
7
+
8
+ if (deletedPaths.length === 0) {
9
+ console.log('No cache to clear')
10
+ } else if (deletedPaths.length === 1) {
11
+ console.log(`Deleted: ${deletedPaths[0]}`)
12
+ } else {
13
+ console.log('Deleted:\n', deletedPaths.join('\n'))
14
+ }
15
+ }
@@ -0,0 +1,5 @@
1
+ import { getCacheDirectoryPath } from './utils/get-cache-dir'
2
+
3
+ export function showCacheDirectory(relativeCacheDirectory: string | undefined) {
4
+ console.log(getCacheDirectoryPath(relativeCacheDirectory))
5
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+
3
+ import hardRejection from 'hard-rejection'
4
+ import sade from 'sade'
5
+
6
+ import { version } from '../package.json'
7
+
8
+ import { clearCacheDirectory } from './cache-clear'
9
+ import { showCacheDirectory } from './cache-dir'
10
+ import { runCommand } from './run'
11
+
12
+ hardRejection()
13
+
14
+ const program = sade('cache-cmd')
15
+
16
+ program
17
+ .version(version)
18
+ .describe('Run and cache a command based on various factors')
19
+ .option(
20
+ '-c, --cache-dir',
21
+ 'Cache directory to use (default: .cache/cache-cmd in nearest node_modules)'
22
+ )
23
+
24
+ program
25
+ .command(
26
+ 'run <command>',
27
+ 'Run cached command (if no <command> provided, this is the default)',
28
+ { default: true }
29
+ )
30
+ .option('-f, --file', 'Run command only when file content changes')
31
+ .option('-t, --time', 'Run command only after specified time (unit with s,m,h,d,w,mo,y)')
32
+ .option('--cache-on-error', 'Cache command run even when command exits with non-zero exit code')
33
+ .example('run "echo ran this command" --time 20s')
34
+ .example('run "./may-fail" --time 20s --cache-on-error')
35
+ .example('run "yarn install" --file yarn.lock')
36
+ .example('run "yarn install" --file yarn.lock --cache-dir .config/cache')
37
+ .example('run "yarn install" --time 1mo --file yarn.lock --file package.json')
38
+ .action((command: unknown, options: Record<string, unknown>) => {
39
+ const cacheDirectory = options['cache-dir']
40
+ const time = options.time
41
+ const file = options.file
42
+ const shouldCacheOnError = options['cache-on-error']
43
+ const files: unknown[] = file === undefined ? [] : Array.isArray(file) ? file : [file]
44
+
45
+ if (typeof command !== 'string') {
46
+ throw Error('Invalid <command> supplied')
47
+ }
48
+
49
+ if (cacheDirectory !== undefined && typeof cacheDirectory !== 'string') {
50
+ throw Error('Invalid --cache-dir supplied')
51
+ }
52
+
53
+ if (time !== undefined && typeof time !== 'string') {
54
+ throw Error('Invalid --time supplied')
55
+ }
56
+
57
+ if (files.some((file) => typeof file !== 'string')) {
58
+ throw Error('Invalid --file supplied')
59
+ }
60
+
61
+ if (shouldCacheOnError !== undefined && typeof shouldCacheOnError !== 'boolean') {
62
+ throw Error('Invalid --cache-on-error supplied')
63
+ }
64
+
65
+ runCommand({
66
+ relativeCacheDirectory: cacheDirectory,
67
+ command,
68
+ cacheByTime: time,
69
+ cacheByFiles: files as string[],
70
+ shouldCacheOnError,
71
+ })
72
+ })
73
+
74
+ program
75
+ .command('cache dir', 'Show cache directory path used by cache-cmd')
76
+ .action((options: Record<string, unknown>) => {
77
+ const cacheDirectory = options['cache-dir']
78
+
79
+ if (cacheDirectory !== undefined && typeof cacheDirectory !== 'string') {
80
+ throw Error('Invalid --cache-dir supplied')
81
+ }
82
+
83
+ showCacheDirectory(cacheDirectory)
84
+ })
85
+
86
+ program
87
+ .command('cache clear', 'Clear cache used by cache-cmd')
88
+ .action((options: Record<string, unknown>) => {
89
+ const cacheDirectory = options['cache-dir']
90
+
91
+ if (cacheDirectory !== undefined && typeof cacheDirectory !== 'string') {
92
+ throw Error('Invalid --cache-dir supplied')
93
+ }
94
+
95
+ clearCacheDirectory(cacheDirectory)
96
+ })
97
+
98
+ program.parse(process.argv)
package/src/run.ts ADDED
@@ -0,0 +1,83 @@
1
+ import { add, isAfter } from 'date-fns'
2
+ import execSh from 'exec-sh'
3
+ import flatCache from 'flat-cache'
4
+ import { isEqual } from 'lodash'
5
+
6
+ import { createCache } from './utils/get-cache-dir'
7
+ import { getCacheKey } from './utils/get-cache-key'
8
+ import { getDuration } from './utils/get-duration'
9
+ import { getFileHashes } from './utils/get-file-hashes'
10
+ import { getFilePaths } from './utils/get-file-paths'
11
+
12
+ interface RunCommandProps {
13
+ relativeCacheDirectory: string | undefined
14
+ command: string
15
+ cacheByTime: string | undefined
16
+ cacheByFiles: string[]
17
+ shouldCacheOnError: boolean | undefined
18
+ }
19
+
20
+ export async function runCommand({
21
+ relativeCacheDirectory,
22
+ command,
23
+ cacheByTime: cacheByTime,
24
+ cacheByFiles: cacheByFiles,
25
+ shouldCacheOnError,
26
+ }: RunCommandProps) {
27
+ if (!cacheByTime && cacheByFiles.length === 0) {
28
+ await execSh.promise(command)
29
+ return
30
+ }
31
+
32
+ const cache = flatCache.load('commands-cache.json', createCache(relativeCacheDirectory))
33
+ const filePaths = getFilePaths(cacheByFiles)
34
+ const duration = cacheByTime ? getDuration(cacheByTime) : undefined
35
+
36
+ const cacheKey = getCacheKey({ duration, filePaths, command })
37
+
38
+ const cacheData: unknown = cache.getKey(cacheKey)
39
+
40
+ const fileHashes = filePaths.length === 0 ? undefined : await getFileHashes(filePaths)
41
+ const currentDate = new Date()
42
+
43
+ const areFileHashesEqual = isEqual((cacheData as any)?.fileHashes, fileHashes)
44
+ const isWithinCacheTime = (() => {
45
+ if (!duration) {
46
+ return true
47
+ }
48
+
49
+ const lastRun = (cacheData as any)?.lastRun
50
+
51
+ if (!lastRun) {
52
+ return false
53
+ }
54
+
55
+ return isAfter(add(new Date(lastRun), duration), currentDate)
56
+ })()
57
+
58
+ if (areFileHashesEqual && isWithinCacheTime) {
59
+ return
60
+ }
61
+
62
+ let execPromise = execSh.promise(command)
63
+
64
+ if (shouldCacheOnError) {
65
+ execPromise = execPromise.catch((error: unknown) => {
66
+ cache.setKey(cacheKey, {
67
+ lastRun: currentDate,
68
+ fileHashes,
69
+ })
70
+ cache.save(true)
71
+
72
+ throw error
73
+ })
74
+ }
75
+
76
+ await execPromise
77
+
78
+ cache.setKey(cacheKey, {
79
+ lastRun: currentDate,
80
+ fileHashes,
81
+ })
82
+ cache.save(true)
83
+ }
@@ -0,0 +1,36 @@
1
+ import path from 'path'
2
+
3
+ import findCacheDir from 'find-cache-dir'
4
+ import makeDir from 'make-dir'
5
+
6
+ import { name } from '../../package.json'
7
+
8
+ export function getCacheDirectoryPath(relativeCacheDirectoryPath: string | undefined) {
9
+ if (relativeCacheDirectoryPath) {
10
+ return path.resolve(process.cwd(), relativeCacheDirectoryPath)
11
+ }
12
+
13
+ const resolvedCacheDirectory = findCacheDir({ name })
14
+
15
+ if (!resolvedCacheDirectory) {
16
+ throw Error('Could not find cache directory. Please provide a cache directory manually.')
17
+ }
18
+
19
+ return resolvedCacheDirectory
20
+ }
21
+
22
+ export function createCache(relativeCacheDirectoryPath: string | undefined) {
23
+ if (relativeCacheDirectoryPath) {
24
+ const absoluteCacheDirectoryPath = makeDir.sync(relativeCacheDirectoryPath)
25
+
26
+ return absoluteCacheDirectoryPath
27
+ }
28
+
29
+ const resolvedCachePath = findCacheDir({ name, create: true })
30
+
31
+ if (!resolvedCachePath) {
32
+ throw Error('Could not find cache directory. Please provide a cache directory manually.')
33
+ }
34
+
35
+ return resolvedCachePath
36
+ }
@@ -0,0 +1,26 @@
1
+ interface GetCacheKeyProps {
2
+ duration: Duration | undefined
3
+ filePaths: string[]
4
+ command: string
5
+ }
6
+
7
+ interface Duration {
8
+ years: number | undefined
9
+ months: number | undefined
10
+ weeks: number | undefined
11
+ days: number | undefined
12
+ hours: number | undefined
13
+ minutes: number | undefined
14
+ seconds: number | undefined
15
+ }
16
+
17
+ export function getCacheKey({ duration, filePaths, command }: GetCacheKeyProps) {
18
+ return [
19
+ 'cwd:' + process.cwd(),
20
+ 'cmd:' + command,
21
+ duration && 'time:' + JSON.stringify(duration),
22
+ filePaths.length !== 0 && 'files:' + filePaths.join(','),
23
+ ]
24
+ .filter(Boolean)
25
+ .join(' ---')
26
+ }
@@ -0,0 +1,44 @@
1
+ export function getDuration(durationString: string) {
2
+ if (!/^(\d+[a-z]+)+$/gi.test(durationString)) {
3
+ throw Error(`Invalid duration: ${durationString}`)
4
+ }
5
+
6
+ const duration = {
7
+ years: undefined as undefined | number,
8
+ months: undefined as undefined | number,
9
+ weeks: undefined as undefined | number,
10
+ days: undefined as undefined | number,
11
+ hours: undefined as undefined | number,
12
+ minutes: undefined as undefined | number,
13
+ seconds: undefined as undefined | number,
14
+ }
15
+
16
+ durationString.match(/\d+[a-z]+/gi)!.forEach((durationPart) => {
17
+ const [, stringQuantity, unitShort] = /(\d+)([a-z]+)/gi.exec(durationPart)!
18
+ const quantity = Number(stringQuantity)
19
+
20
+ const unitLong = (
21
+ {
22
+ y: 'years',
23
+ mo: 'months',
24
+ w: 'weeks',
25
+ d: 'days',
26
+ h: 'hours',
27
+ m: 'minutes',
28
+ s: 'seconds',
29
+ } as const
30
+ )[unitShort!]
31
+
32
+ if (Number.isNaN(quantity) || !unitLong) {
33
+ throw Error(`Invalid duration part: ${durationPart}`)
34
+ }
35
+
36
+ if (duration[unitLong] !== undefined) {
37
+ throw Error(`Duration with unit ${unitLong} was supplied multiple times`)
38
+ }
39
+
40
+ duration[unitLong] = quantity
41
+ })
42
+
43
+ return duration
44
+ }
@@ -0,0 +1,13 @@
1
+ import hasha from 'hasha'
2
+
3
+ export async function getFileHashes(filePaths: string[]) {
4
+ return Object.fromEntries(
5
+ await Promise.all(
6
+ filePaths.map((filePath) =>
7
+ hasha
8
+ .fromFile(filePath, { algorithm: 'md5' })
9
+ .then((hash) => [filePath, hash] as const)
10
+ )
11
+ )
12
+ )
13
+ }
@@ -0,0 +1,7 @@
1
+ import path from 'path'
2
+
3
+ export function getFilePaths(relativeFilePaths: string[]) {
4
+ const cwd = process.cwd()
5
+
6
+ return relativeFilePaths.map((file) => path.resolve(cwd, file)).sort()
7
+ }