diff --git a/.cherry.cjs b/.cherry.cjs index 9682d6b9..c741d46a 100644 --- a/.cherry.cjs +++ b/.cherry.cjs @@ -1,5 +1,5 @@ -const JS_FILES = 'app/**/*.{js,jsx}' -const TS_FILES = 'app/**/*.{ts,tsx}' +const JS_FILES = '**/*.{js,jsx}' +const TS_FILES = '**/*.{ts,tsx}' module.exports = { project_name: 'fwuensche/cherry-cli', @@ -15,5 +15,15 @@ module.exports = { name: 'TODO', pattern: /TODO/, }, + { + name: '[TS Migration] TS lines of code', + include: TS_FILES, + groupByFile: true, + }, + { + name: '[TS Migration] JS lines of code', + include: JS_FILES, + groupByFile: true, + }, ], } diff --git a/.github/workflows/cherry_diff.yml b/.github/workflows/cherry_diff.yml index 08e10127..6b6a9627 100644 --- a/.github/workflows/cherry_diff.yml +++ b/.github/workflows/cherry_diff.yml @@ -17,4 +17,6 @@ jobs: run: npm install - name: Raise if new JavaScript code is added + # This command will fail if the number of lines of code in JavaScript files has increased + # in the current branch compared to the base branch, encouraging developers to contribute to migrating to TS. run: ./bin/cherry.js diff --metric='[loc] JavaScript' --error-if-increase --quiet diff --git a/src/helpers/console.js b/src/helpers/console.js new file mode 100644 index 00000000..4826f306 --- /dev/null +++ b/src/helpers/console.js @@ -0,0 +1,6 @@ +const YELLOW = '\x1B[33m' +const RESET = '\x1B[0m' + +export function warn(message) { + console.warn(`${YELLOW}⚠️ ${message}${RESET}`) +} diff --git a/src/helpers/timer.js b/src/helpers/timer.js new file mode 100644 index 00000000..d91b3bc4 --- /dev/null +++ b/src/helpers/timer.js @@ -0,0 +1,37 @@ +import { warn } from './console.js' + +let timers = {} + +/** + * Executes a provided function block and measures its execution time. + * Logs a message if the execution time exceeds 2 seconds. + * + * @param {Function} codeBlock - The block of code to execute. + * @returns {*} The result of the executed code block. + */ +export async function executeWithTiming(codeBlock, identifier) { + const startTime = performance.now() + + const result = await codeBlock() + + const endTime = performance.now() + const executionTime = endTime - startTime + + timers[identifier] = executionTime + + return result +} + +/** + * Logs a warning for each long running task. + * A task is considered long running if it takes longer than the provided time limit. + * + * @param {number} timeLimitInMs - The time limit in milliseconds. + */ +export function warnsAboutLongRunningTasks(timeLimitInMs) { + for (const [identifier, executionTime] of Object.entries(timers).sort()) { + if (executionTime > timeLimitInMs) { + warn(`${identifier} took ${Math.round(executionTime)}ms`) + } + } +} diff --git a/src/occurrences.js b/src/occurrences.js index 54191b32..48fb18e5 100644 --- a/src/occurrences.js +++ b/src/occurrences.js @@ -11,6 +11,7 @@ import loc from './plugins/loc.js' import npmOutdated from './plugins/npm_outdated.js' import rubocop from './plugins/rubocop.js' import yarnOutdated from './plugins/yarn_outdated.js' +import { executeWithTiming, warnsAboutLongRunningTasks } from './helpers/timer.js' const spinnies = new Spinnies() @@ -85,9 +86,14 @@ const matchPatterns = (files, metrics, quiet) => { if (!files.length || !metrics.length) return [] if (!quiet) spinnies.add('patterns', { text: 'Matching patterns...', indent: 2 }) + // Limit number of concurrently opened files to avoid "Error: spawn EBADF" const limit = pLimit(10) - const promise = Promise.all(files.map((file) => limit(() => findFileOccurences(file, metrics)))) + const promise = executeWithTiming( + () => Promise.all(files.map((file) => limit(() => findFileOccurences(file, metrics)))), + 'All pattern metrics together' + ) + if (!quiet) promise.then(() => spinnies.succeed('patterns', { text: 'Matching patterns' })) return promise @@ -97,17 +103,22 @@ const runEvals = (metrics, codeOwners, quiet) => { if (!metrics.length) return [] if (!quiet) spinnies.add('evals', { text: 'Running eval()...', indent: 2 }) + const promise = Promise.all( metrics.map(async (metric) => { - if (!quiet) + if (!quiet) { spinnies.add(`metric_${metric.name}`, { text: `${metric.name}...`, indent: 4, }) - const result = (await metric.eval({ codeOwners })).map((occurrence) => ({ - ...occurrence, - metricName: metric.name, - })) + } + + const occurrences = await executeWithTiming( + async () => await metric.eval({ codeOwners }), + `Metric '${metric.name}'` + ) + const result = occurrences.map((occurrence) => ({ ...occurrence, metricName: metric.name })) + if (!quiet) spinnies.succeed(`metric_${metric.name}`, { text: metric.name }) return result }) @@ -126,7 +137,7 @@ const runPlugins = async (plugins, quiet) => { const plugin = PLUGINS[name] if (!plugin) panic(`Unsupported '${name}' plugin\nExpected one of: ${Object.keys(PLUGINS).join(', ')}`) if (!quiet) spinnies.add(`plugin_${name}`, { text: `${name}...`, indent: 4 }) - const result = await plugin.run(options) + const result = executeWithTiming(async () => await plugin.run(options), `Plugin '${name}'`) if (!quiet) spinnies.succeed(`plugin_${name}`, { text: name }) return result }) @@ -157,21 +168,21 @@ export const findOccurrences = async ({ configuration, files, metric, codeOwners // From ['loc'] to { 'loc': {} } to handle deprecated array configuration for plugins if (Array.isArray(plugins)) plugins = plugins.reduce((acc, value) => ({ ...acc, [value]: {} }), {}) - const promise = Promise.all([ + const result = await Promise.all([ matchPatterns(files, fileMetrics, quiet), runEvals(evalMetrics, codeOwners, quiet), runPlugins(plugins, quiet), ]) - const occurrences = _.flattenDeep(await promise).map( - ({ text, value, metricName, filePath, lineNumber, url, owners }) => ({ - text, - value, - metricName, - url: url !== undefined ? url : filePath && buildPermalink(configuration.project_name, filePath, lineNumber), - owners: owners !== undefined ? owners : filePath && codeOwners.getOwners(filePath), - }) - ) + warnsAboutLongRunningTasks(5000) + + const occurrences = _.flattenDeep(result).map(({ text, value, metricName, filePath, lineNumber, url, owners }) => ({ + text, + value, + metricName, + url: url !== undefined ? url : filePath && buildPermalink(configuration.project_name, filePath, lineNumber), + owners: owners !== undefined ? owners : filePath && codeOwners.getOwners(filePath), + })) return withEmptyMetrics(occurrences, metrics) }