nhb-scripts
Version:
A collection of Node.js scripts to use in TypeScript & JavaScript projects
214 lines (189 loc) ⢠6.05 kB
JavaScript
// bin/count.mjs
// @ts-check
import { intro, outro, text } from '@clack/prompts';
import chalk from 'chalk';
import fs from 'fs/promises';
import { extname, join, resolve } from 'path';
import tsModule from 'typescript';
import { addPipeOnLeft, normalizeStringResult } from '../lib/clack-utils.mjs';
import { loadUserConfig } from '../lib/config-loader.mjs';
/**
* @typedef {Object} Exports
* @property {number} default Default export count.
* @property {number} namedExportsTotal Total named export count.
* @property {number} namedExportsDirect Original named export count.
* @property {number} namedExportsAliased Aliased named export count.
* @property {number} namedTypeExports Total named type export count.
*/
/**
* Prompt the user to enter a JS/TS/MJS path (file or folder)
* @returns {Promise<string>} The resolved path
*/
async function getFilePath() {
intro(chalk.cyan.bold('š Export Counter'));
const defaultPath = (await loadUserConfig()).count?.defaultPath ?? '.';
const inputPath = normalizeStringResult(
await text({
message: chalk.gray(
chalk.cyanBright.bold(
`šÆ Please specify the path to a ${chalk.yellowBright('"js/ts/mjs"')} file or a folder containing ${chalk.yellowBright('"js/ts/mjs"')} files.\n`
) +
addPipeOnLeft(
' - Enter the full file path (with extension) to process a specific file.\n'
) +
addPipeOnLeft(
` - Enter a folder path to scan all ${chalk.bold.yellowBright('*.js')}, ${chalk.bold.yellowBright('*.ts')}, or ${chalk.bold.yellowBright('*.mjs')} files within.\n`
) +
addPipeOnLeft(
` - Leave it empty to scan the default folder/file: ${chalk.bgYellowBright.bold.whiteBright(defaultPath)}\n`
) +
addPipeOnLeft()
),
placeholder: `e.g. ${chalk.yellowBright('src/app')} or ${chalk.yellowBright('src/index.ts')}`,
})
);
const filePath = inputPath || defaultPath;
return resolve(filePath);
}
/**
* Count types of exports in a JS/TS file
* @param {string} filePath
* @returns {Promise<Exports>}
*/
async function countExports(filePath) {
try {
const content = await fs.readFile(filePath, 'utf-8');
const sourceFile = tsModule.createSourceFile(
filePath,
content,
tsModule.ScriptTarget.Latest,
true
);
let namedExportsTotal = 0;
let defaultExports = 0;
let aliasedExports = 0;
let namedTypeExports = 0;
/** @param {import('typescript').Node} node */
const checkNode = (node) => {
if (tsModule.isExportAssignment(node)) {
if (!node.isExportEquals) defaultExports += 1;
} else if (
tsModule.isFunctionDeclaration(node) ||
tsModule.isClassDeclaration(node) ||
tsModule.isInterfaceDeclaration(node) ||
tsModule.isEnumDeclaration(node) ||
tsModule.isTypeAliasDeclaration(node) ||
tsModule.isVariableStatement(node)
) {
if (
node.modifiers?.some(
(m) => m.kind === tsModule.SyntaxKind.ExportKeyword
)
) {
if (
node.modifiers.some(
(m) => m.kind === tsModule.SyntaxKind.DefaultKeyword
)
) {
defaultExports += 1;
} else {
namedExportsTotal += 1;
}
}
} else if (
tsModule.isExportDeclaration(node) &&
node.exportClause &&
tsModule.isNamedExports(node.exportClause)
) {
if (node.isTypeOnly) {
namedTypeExports += node.exportClause.elements.length;
} else {
namedExportsTotal += node.exportClause.elements.length;
for (const el of node.exportClause.elements) {
if (el.propertyName && el.propertyName.text !== el.name.text) {
aliasedExports += 1;
}
}
}
}
tsModule.forEachChild(node, checkNode);
};
tsModule.forEachChild(sourceFile, checkNode);
return {
default: defaultExports,
namedExportsTotal,
namedExportsDirect: namedExportsTotal - aliasedExports,
namedExportsAliased: aliasedExports,
namedTypeExports,
};
} catch (err) {
console.error(chalk.red('š Failed to parse or read file:\n'), err);
process.exit(0);
}
}
/**
* Recursively scan a folder for .js/.ts/.mjs
* @param {string} folderPath
* @returns {Promise<string[]>}
*/
async function getFilesFromFolder(folderPath) {
const files = await fs.readdir(folderPath, { withFileTypes: true });
/** @type {string[]} */
let filePaths = [];
for (const file of files) {
const fullPath = join(folderPath, file.name);
// Skip node_modules and dist directories
if (file.isDirectory()) {
const excludePaths = (await loadUserConfig()).count?.excludePaths ?? [
'node_modules',
'dist',
'build',
];
if (excludePaths.includes(file.name)) {
continue;
}
filePaths = filePaths.concat(await getFilesFromFolder(fullPath));
} else if (['.js', '.ts', '.mjs'].includes(extname(file.name))) {
filePaths.push(fullPath);
}
}
return filePaths;
}
// Main Execution
(async () => {
try {
const filePath = await getFilePath();
const stats = await fs.stat(filePath);
let filesToProcess = [];
if (stats.isDirectory()) {
filesToProcess = await getFilesFromFolder(filePath);
if (filesToProcess.length === 0) {
throw new Error('No `.js`, `.mjs` or `.ts` files found in the folder.');
}
} else {
filesToProcess = [filePath];
}
for (const file of filesToProcess) {
const result = await countExports(file);
console.info(chalk.green(`\nš¦ Export Summary for "${file}":`));
console.info(chalk.yellow(`šø Default Exports : ${result.default}`));
console.info(
chalk.yellow(`š¹ Named Exports (Total) : ${result.namedExportsTotal}`)
);
console.info(
chalk.yellow(` ⣠Direct : ${result.namedExportsDirect}`)
);
console.info(
chalk.yellow(` ā Aliased : ${result.namedExportsAliased}`)
);
console.info(
chalk.yellow(`šŗ Total Type Exports : ${result.namedTypeExports}`)
);
}
outro(chalk.green('š Scan completed!'));
} catch (error) {
console.error(chalk.red('š Unexpected Error:\n'), error);
process.exit(0);
}
})();