UNPKG

tiny-essentials

Version:

Collection of small, essential scripts designed to be used across various projects. These simple utilities are crafted for speed, ease of use, and versatility.

696 lines (625 loc) 25 kB
#!/usr/bin/env node import { copyFile, readFile, mkdir, writeFile } from 'fs/promises'; import { existsSync } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { parse } from '@babel/parser'; import _traverse from '@babel/traverse'; import _generate from '@babel/generator'; import * as t from '@babel/types'; // Workaround for Babel ESM compatibility const traverse = _traverse.default || _traverse; const generate = _generate.default || _generate; const LATEST_VERSION = 1; /** * Defines the current execution paths. * @typedef {Object} AppPaths * @property {string} cliRoot - The root directory of tiny-essentials. * @property {string} userProjectRoot - The root directory of the user's project invoking the CLI. */ const PATHS = { cliRoot: path.dirname(fileURLToPath(import.meta.url)), userProjectRoot: process.cwd(), /** * Resolves the output directory. * @param {string|null} customPath - Optional custom path provided by the user. * @returns {string} The absolute path for the vendor directory. */ getVendorDir(customPath) { return customPath ? path.resolve(this.userProjectRoot, customPath) : path.join(this.userProjectRoot, 'vendor', 'tiny-essentials'); }, }; /** * Parses a single user input target into resolving segments. * * @param {string} rawTarget - The raw string input from CLI. * @returns {Object} Target descriptors. * @property {number|null} targetVersion - The explicit version if provided. * @property {string} basePath - The relative path requested. * @property {string[]} functionsToExtract - The specific functions to pull via Babel. */ function parseTarget(rawTarget) { /** * Extracted version from the target string. * @type {number|null} */ let targetVersion = null; /** * The target string without the version prefix. * @type {string} */ let cleanTarget = rawTarget; /** * Regex match result for versioning. * @type {RegExpMatchArray|null} */ const versionMatch = rawTarget.match(/^v(\d+)\//); if (versionMatch) { targetVersion = parseInt(versionMatch[1], 10); cleanTarget = rawTarget.replace(versionMatch[0], ''); } /** * Indicates if specific functions are requested. * @type {boolean} */ const hasFunctions = cleanTarget.includes(','); /** * List of specific function names to extract. * @type {string[]} */ let functionsToExtract = []; /** * The base file path to search for. * @type {string} */ let basePath = cleanTarget; if (hasFunctions) { const parts = cleanTarget.split('/'); const lastPart = parts.pop(); if (lastPart) { functionsToExtract = lastPart.split(',').map((f) => f.trim()); basePath = parts.join('/'); } } return { targetVersion, basePath, functionsToExtract }; } /** * Searches for the requested module file across versions. * * @param {number|null} targetVersion - The optional hardcoded version. * @param {string} basePath - The module path. * @returns {Promise<{ filePath: string, ext: string, versionDir: string, interceptedFunction?: string }>} Resolved file info. */ async function resolveSourceFile(targetVersion, basePath) { if (basePath.includes('/build/')) { throw new Error('Forbidden extraction: Cannot export files from the build directory.'); } /** * Supported file extensions. * @type {string[]} */ const extensions = ['.mjs', '.js']; /** * The starting version for the resolution search. * @type {number} */ const startVersion = targetVersion || LATEST_VERSION; /** * The ending version for the resolution search limit. * @type {number} */ const endVersion = targetVersion ? targetVersion : 1; for (let v = startVersion; v >= endVersion; v--) { const versionDir = path.join(PATHS.cliRoot, 'dist', `v${v}`); // If it's a direct file path without functions (which means basePath might be the full path) for (const ext of extensions) { // First attempt: Treating basePath as the exact file (minus extension) const exactPath = path.join(versionDir, `${basePath}${ext}`); if (existsSync(exactPath)) return { filePath: exactPath, ext, versionDir }; // Second attempt: Checking if the last folder segment in basePath was meant to be functions, // but didn't have commas. (e.g., folder/module/func1) const parts = basePath.split('/'); if (parts.length > 1) { const funcNameCandidate = parts.pop(); const modulePath = parts.join('/'); const nestedPath = path.join(versionDir, `${modulePath}${ext}`); if (existsSync(nestedPath)) { // Returns the path, but caller needs to know it intercepted a single function return { filePath: nestedPath, ext, versionDir, interceptedFunction: funcNameCandidate }; } } } } throw new Error(`Module not found: ${basePath}`); } /** * Copies the LICENSE file from the project root to the destination vendor directory. * * @param {string} destDir - The final vendor output directory. * @returns {Promise<void>} Resolves when the file is copied (or skipped if missing). */ async function copyLicense(destDir) { /** * Absolute path to the original LICENSE file. * @type {string} */ const licenseSrc = path.join(PATHS.cliRoot, 'LICENSE'); /** * Absolute path to the destination LICENSE file. * @type {string} */ const licenseDest = path.join(destDir, 'LICENSE'); try { if (existsSync(licenseSrc)) { await copyFile(licenseSrc, licenseDest); console.log('> Copied: LICENSE'); } else { console.warn('> Warning: LICENSE file not found in the root directory.'); } } catch (error) { console.error(`> Error copying LICENSE: ${error.message}`); } } /** * Advanced Multi-File Dependency Extractor. * Parses files, traces dependencies, and extracts everything mirroring the original structure. */ class MultiFileExtractor { /** * @param {string} outDir - The base output directory. */ constructor(outDir) { this.outDir = outDir; this.fileRequests = new Map(); this.processedFiles = new Map(); } /** * Registers a file and the specific functions needed from it into the extraction queue. * * @param {string} filePath - Absolute path to the file. * @param {string[]} names - Specific exports to keep (empty array means full file). * @param {string} versionDir - The base version directory for structural mirroring. */ requestExtraction(filePath, names, versionDir) { if (!this.fileRequests.has(filePath)) { this.fileRequests.set(filePath, { names: new Set(), versionDir, full: false }); } const req = this.fileRequests.get(filePath); if (names.length === 0) { req.full = true; } else { names.forEach((n) => req.names.add(n)); } } /** * Resolves local imports relative to the current file. * * @param {string} currentFilePath - The file importing the module. * @param {string} importStr - The import string. * @returns {string|null} Resolved absolute path or null if it's an external package. */ resolveImportPath(currentFilePath, importStr) { if (!importStr.startsWith('.')) return null; // Ignore external/node_modules const dir = path.dirname(currentFilePath); let resolved = path.resolve(dir, importStr); if (!path.extname(resolved)) { for (const ext of ['.mjs', '.js']) { if (existsSync(resolved + ext)) return resolved + ext; } } return resolved; } async processQueue() { let processedCount = 0; // Use a while loop because tracing dependencies will dynamically add new files to the Map while (processedCount < this.fileRequests.size) { const entries = Array.from(this.fileRequests.entries()); for (const [filePath, req] of entries) { if (this.processedFiles.has(filePath)) continue; const finalCode = await this.parseAndPrune(filePath, req); this.processedFiles.set(filePath, { code: finalCode, versionDir: req.versionDir }); processedCount++; } } await this.writeFiles(); } /** * Core AST manipulation: traces required imports and removes unused code. * * @param {string} filePath - Target file path. * @param {Object} req - Request object containing required export names. * @returns {Promise<string>} The minified and formatted generated source code. */ async parseAndPrune(filePath, req) { const sourceCode = await readFile(filePath, 'utf-8'); const ast = parse(sourceCode, { sourceType: 'module', plugins: ['classProperties'] }); const localKeepNames = new Set(); const requiredImports = new Map(); // Phase 1: Deep traceability traverse(ast, { Program: (astPath) => { const bindings = astPath.scope.bindings; if (req.full) { Object.keys(bindings).forEach((name) => localKeepNames.add(name)); } const queue = []; const addToKeep = (name) => { if (!localKeepNames.has(name)) { localKeepNames.add(name); queue.push(name); } }; astPath.traverse({ // Tracing Static Methods on any Class ClassDeclaration(classPath) { if (req.full) return; classPath.node.body.body.forEach((item) => { const keyName = t.isIdentifier(item.key) ? item.key.name : null; if ( keyName && item.static && (t.isClassMethod(item) || t.isClassProperty(item)) && req.names.has(keyName) ) { traverse( item, { Identifier(idPath) { if (idPath.isReferencedIdentifier() && bindings[idPath.node.name]) { addToKeep(idPath.node.name); } }, }, astPath.scope, astPath, ); } }); }, CallExpression(callPath) { if (callPath.node.callee.type === 'Import') { const arg = callPath.node.arguments[0]; if (t.isStringLiteral(arg)) { const source = arg.value; if (!requiredImports.has(source)) requiredImports.set(source, new Set()); requiredImports.get(source).add({ local: '*', imported: '*' }); } } }, ImportDeclaration(expPath) { const source = expPath.node.source.value; if (!requiredImports.has(source)) requiredImports.set(source, new Set()); if (req.full) { if (expPath.node.specifiers.length === 0) { requiredImports.get(source).add({ local: null, imported: null }); } else { expPath.node.specifiers.forEach((spec) => { const importedName = t.isImportDefaultSpecifier(spec) ? 'default' : t.isImportNamespaceSpecifier(spec) ? '*' : spec.imported.name; requiredImports .get(source) .add({ local: spec.local.name, imported: importedName }); }); } } }, // Catch re-exports (export * from './module') ExportAllDeclaration(expPath) { if (expPath.node.source) { const source = expPath.node.source.value; if (!requiredImports.has(source)) requiredImports.set(source, new Set()); // Always request '*' to trace export everything if needed requiredImports.get(source).add({ local: '*', imported: '*', isExportAll: true }); } }, ExportNamedDeclaration(expPath) { if (expPath.node.source) { // Tracing specific re-exports: export { target } from './module' const source = expPath.node.source.value; if (!requiredImports.has(source)) requiredImports.set(source, new Set()); expPath.node.specifiers.forEach((spec) => { const exportedName = spec.exported.name; if (req.full || req.names.has(exportedName)) { const importedName = spec.local ? spec.local.name : exportedName; requiredImports .get(source) .add({ local: null, imported: importedName, isReExport: true, exportedName }); } }); } else { const dec = expPath.node.declaration; if (dec) { if (t.isFunctionDeclaration(dec) && (req.full || req.names.has(dec.id.name))) { addToKeep(dec.id.name); } else if (t.isVariableDeclaration(dec)) { dec.declarations.forEach((d) => { if (t.isIdentifier(d.id) && (req.full || req.names.has(d.id.name))) addToKeep(d.id.name); }); } } else if (expPath.node.specifiers) { expPath.node.specifiers.forEach((spec) => { if (req.full || req.names.has(spec.exported.name)) addToKeep(spec.local.name); }); } } }, ExportDefaultDeclaration(expPath) { const dec = expPath.node.declaration; if (req.full || req.names.has('default')) { if (t.isIdentifier(dec)) addToKeep(dec.name); else if (dec.id) addToKeep(dec.id.name); } }, }); // Trace local variables and imports inside the queue let i = 0; while (i < queue.length) { const currentName = queue[i++]; const binding = bindings[currentName]; if (binding) { // Namespace Specifier Fix Included! if ( binding.path.isImportSpecifier() || binding.path.isImportDefaultSpecifier() || binding.path.isImportNamespaceSpecifier() ) { const importDecl = binding.path.parentPath.node; const source = importDecl.source.value; if (!requiredImports.has(source)) requiredImports.set(source, new Set()); const importedName = binding.path.isImportDefaultSpecifier() ? 'default' : binding.path.isImportNamespaceSpecifier() ? '*' : binding.path.node.imported.name; requiredImports.get(source).add({ local: currentName, imported: importedName }); } else { binding.path.traverse({ Identifier(idPath) { if (idPath.isReferencedIdentifier() && bindings[idPath.node.name]) { addToKeep(idPath.node.name); } }, }); } } } }, }); // Trigger extraction of all found deep dependencies for (const [source, specs] of requiredImports.entries()) { const resolvedPath = this.resolveImportPath(filePath, source); if (resolvedPath) { // If it's a full export dependency (like side-effect imports or export *), request full extraction const isFullDependency = Array.from(specs).some( (s) => s.imported === null || s.imported === '*', ); const importedNamesToRequest = isFullDependency ? [] : Array.from(specs).map((s) => s.imported); this.requestExtraction(resolvedPath, importedNamesToRequest, req.versionDir); } } // Phase 2: Surgical Pruning if (!req.full) { // Helper function to extract Class Methods AND Class Properties natively const extractStaticMethods = (classNode) => { const newNodes = []; classNode.body.body.forEach((item) => { const keyName = t.isIdentifier(item.key) ? item.key.name : null; if ( keyName && item.static && (t.isClassMethod(item) || t.isClassProperty(item)) && req.names.has(keyName) ) { if (t.isClassMethod(item)) { const func = t.functionDeclaration( t.identifier(keyName), item.params, item.body, item.generator, item.async, ); if (item.leadingComments) func.leadingComments = item.leadingComments; newNodes.push(t.exportNamedDeclaration(func)); } else if (t.isClassProperty(item)) { const variableDec = t.variableDeclaration('const', [ t.variableDeclarator(t.identifier(keyName), item.value), ]); if (item.leadingComments) variableDec.leadingComments = item.leadingComments; newNodes.push(t.exportNamedDeclaration(variableDec)); } } }); return newNodes; }; traverse(ast, { Program(astPath) { const body = astPath.get('body'); for (const statement of body) { let keep = false; if (statement.isImportDeclaration()) { const source = statement.node.source.value; if (requiredImports.has(source)) { const neededSpecs = requiredImports.get(source); const neededLocalNames = new Set( Array.from(neededSpecs) .map((s) => s.local) .filter(Boolean), ); if (statement.node.specifiers.length > 0) { statement.node.specifiers = statement.node.specifiers.filter((spec) => neededLocalNames.has(spec.local.name), ); if (statement.node.specifiers.length > 0) keep = true; } } } else if (statement.isExportNamedDeclaration()) { if (statement.node.source) { const source = statement.node.source.value; if (requiredImports.has(source)) { const neededSpecs = requiredImports.get(source); const neededExportedNames = new Set( Array.from(neededSpecs) .filter((s) => s.isReExport) .map((s) => s.exportedName), ); statement.node.specifiers = statement.node.specifiers.filter((spec) => neededExportedNames.has(spec.exported.name), ); if (statement.node.specifiers.length > 0) keep = true; } } else { const dec = statement.get('declaration'); if ( dec && dec.isFunctionDeclaration() && (req.names.has(dec.node.id.name) || localKeepNames.has(dec.node.id.name)) ) { keep = true; } else if (dec && dec.isVariableDeclaration()) { const keepers = dec.node.declarations.filter( (d) => t.isIdentifier(d.id) && (req.names.has(d.id.name) || localKeepNames.has(d.id.name)), ); if (keepers.length > 0) { dec.node.declarations = keepers; keep = true; } } else if (dec && dec.isClassDeclaration()) { if (req.names.has(dec.node.id.name) || localKeepNames.has(dec.node.id.name)) { keep = true; } else { const extractedNodes = extractStaticMethods(dec.node); if (extractedNodes.length > 0) { statement.replaceWithMultiple(extractedNodes); keep = true; continue; } } } else if (statement.node.specifiers && statement.node.specifiers.length > 0) { statement.node.specifiers = statement.node.specifiers.filter( (spec) => req.names.has(spec.exported.name) || localKeepNames.has(spec.local.name), ); if (statement.node.specifiers.length > 0) keep = true; } } } else if (statement.isExportAllDeclaration()) { const source = statement.node.source.value; if (requiredImports.has(source)) { const neededSpecs = requiredImports.get(source); if (Array.from(neededSpecs).some((s) => s.isExportAll)) keep = true; } } else if (statement.isExportDefaultDeclaration()) { if (req.names.has('default')) { keep = true; } else if (t.isClassDeclaration(statement.node.declaration)) { const extractedNodes = extractStaticMethods(statement.node.declaration); if (extractedNodes.length > 0) { statement.replaceWithMultiple(extractedNodes); keep = true; continue; } } } else if (statement.isClassDeclaration()) { if (localKeepNames.has(statement.node.id.name)) { keep = true; } else { const extractedNodes = extractStaticMethods(statement.node); if (extractedNodes.length > 0) { statement.replaceWithMultiple(extractedNodes); keep = true; continue; } } } else if ( statement.isFunctionDeclaration() && localKeepNames.has(statement.node.id.name) ) { keep = true; } else if (statement.isVariableDeclaration()) { const keepers = statement.node.declarations.filter( (d) => t.isIdentifier(d.id) && localKeepNames.has(d.id.name), ); if (keepers.length > 0) { statement.node.declarations = keepers; keep = true; } } if (!keep) statement.remove(); } }, }); } return generate(ast).code; } /** * Flushes processed files mapping onto the filesystem maintaining origin structure. */ async writeFiles() { let filesSaved = 0; for (const [filePath, data] of this.processedFiles.entries()) { const relativeToVersion = path.relative(data.versionDir, filePath); const destPath = path.join(this.outDir, relativeToVersion); await mkdir(path.dirname(destPath), { recursive: true }); await writeFile(destPath, data.code, 'utf-8'); console.log(`> Created: ${relativeToVersion}`); filesSaved++; } console.log(`\nSuccessfully orchestrated and bundled ${filesSaved} isolated files!`); } } /** * Main execution function for the CLI tool. */ async function runCLI() { const args = process.argv.slice(2); /** * The explicit output directory argument if passed. * @type {string|undefined} */ const outDirArg = args.find((a) => a.startsWith('--out-dir=')); /** * The formatted custom output directory. * @type {string|null} */ const customOutDir = outDirArg ? outDirArg.split('=')[1] : null; // Filter out flags to get the raw target strings const rawTargets = args.filter((a) => !a.startsWith('--')); if (rawTargets.length === 0) { console.error( 'Usage: npx tiny-essentials-fork [--out-dir=custom/path] <target1> [target2] ...', ); console.error('Example: npx tiny-essentials-fork v1/basics/example/func1 v1/lib/TinyHtml'); process.exit(1); } try { const finalVendorDir = PATHS.getVendorDir(customOutDir); const extractor = new MultiFileExtractor(finalVendorDir); for (const rawTarget of rawTargets) { let { targetVersion, basePath, functionsToExtract } = parseTarget(rawTarget); const resolved = await resolveSourceFile(targetVersion, basePath); if (resolved.interceptedFunction && functionsToExtract.length === 0) { functionsToExtract.push(resolved.interceptedFunction); } extractor.requestExtraction(resolved.filePath, functionsToExtract, resolved.versionDir); } console.log(`Starting extraction to: ${finalVendorDir}\n`); await extractor.processQueue(); // Copy the license file explicitly at the very end of processing await copyLicense(finalVendorDir); } catch (error) { console.error('\nError extracting modules:'); console.error(error.message); process.exit(1); } } runCLI();