tiny-essentials 1.25.6 → 1.26.0

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/TinyFork.mjs ADDED
@@ -0,0 +1,608 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs/promises';
4
+ import { existsSync } from 'fs';
5
+ import path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ import parser from '@babel/parser';
8
+ import _traverse from '@babel/traverse';
9
+ import _generate from '@babel/generator';
10
+ import * as t from '@babel/types';
11
+
12
+ // Workaround for Babel ESM compatibility
13
+ const traverse = _traverse.default || _traverse;
14
+ const generate = _generate.default || _generate;
15
+
16
+ const LATEST_VERSION = 1;
17
+
18
+ /**
19
+ * Defines the current execution paths.
20
+ * @typedef {Object} AppPaths
21
+ * @property {string} cliRoot - The root directory of tiny-essentials.
22
+ * @property {string} userProjectRoot - The root directory of the user's project invoking the CLI.
23
+ */
24
+ const PATHS = {
25
+ cliRoot: path.dirname(fileURLToPath(import.meta.url)),
26
+ userProjectRoot: process.cwd(),
27
+ /**
28
+ * Resolves the output directory.
29
+ * @param {string|null} customPath - Optional custom path provided by the user.
30
+ * @returns {string} The absolute path for the vendor directory.
31
+ */
32
+ getVendorDir(customPath) {
33
+ return customPath
34
+ ? path.resolve(this.userProjectRoot, customPath)
35
+ : path.join(this.userProjectRoot, 'vendor', 'tiny-essentials');
36
+ },
37
+ };
38
+
39
+ /**
40
+ * Parses a single user input target into resolving segments.
41
+ *
42
+ * @param {string} rawTarget - The raw string input from CLI.
43
+ * @returns {Object} Target descriptors.
44
+ * @property {number|null} targetVersion - The explicit version if provided.
45
+ * @property {string} basePath - The relative path requested.
46
+ * @property {string[]} functionsToExtract - The specific functions to pull via Babel.
47
+ */
48
+ function parseTarget(rawTarget) {
49
+ let targetVersion = null;
50
+ let cleanTarget = rawTarget;
51
+
52
+ const versionMatch = rawTarget.match(/^v(\d+)\//);
53
+ if (versionMatch) {
54
+ targetVersion = parseInt(versionMatch[1], 10);
55
+ cleanTarget = rawTarget.replace(versionMatch[0], '');
56
+ }
57
+
58
+ const hasFunctions = cleanTarget.includes(',');
59
+ let functionsToExtract = [];
60
+ let basePath = cleanTarget;
61
+
62
+ if (hasFunctions) {
63
+ const parts = cleanTarget.split('/');
64
+ const lastPart = parts.pop();
65
+ if (lastPart) {
66
+ functionsToExtract = lastPart.split(',').map((f) => f.trim());
67
+ basePath = parts.join('/');
68
+ }
69
+ }
70
+
71
+ return { targetVersion, basePath, functionsToExtract };
72
+ }
73
+
74
+ /**
75
+ * Searches for the requested module file across versions.
76
+ *
77
+ * @param {number|null} targetVersion - The optional hardcoded version.
78
+ * @param {string} basePath - The module path.
79
+ * @returns {Promise<{ filePath: string, ext: string, versionDir: string, interceptedFunction?: string }>} Resolved file info.
80
+ */
81
+ async function resolveSourceFile(targetVersion, basePath) {
82
+ if (basePath.includes('/build/')) {
83
+ throw new Error('Forbidden extraction: Cannot export files from the build directory.');
84
+ }
85
+
86
+ const extensions = ['.mjs', '.js'];
87
+ const startVersion = targetVersion || LATEST_VERSION;
88
+ const endVersion = targetVersion ? targetVersion : 1;
89
+
90
+ for (let v = startVersion; v >= endVersion; v--) {
91
+ const versionDir = path.join(PATHS.cliRoot, 'dist', `v${v}`);
92
+
93
+ // If it's a direct file path without functions (which means basePath might be the full path)
94
+ for (const ext of extensions) {
95
+ // First attempt: Treating basePath as the exact file (minus extension)
96
+ const exactPath = path.join(versionDir, `${basePath}${ext}`);
97
+ if (existsSync(exactPath)) return { filePath: exactPath, ext, versionDir };
98
+
99
+ // Second attempt: Checking if the last folder segment in basePath was meant to be functions,
100
+ // but didn't have commas. (e.g., folder/module/func1)
101
+ const parts = basePath.split('/');
102
+ if (parts.length > 1) {
103
+ const funcNameCandidate = parts.pop();
104
+ const modulePath = parts.join('/');
105
+ const nestedPath = path.join(versionDir, `${modulePath}${ext}`);
106
+
107
+ if (existsSync(nestedPath)) {
108
+ // Returns the path, but caller needs to know it intercepted a single function
109
+ return { filePath: nestedPath, ext, versionDir, interceptedFunction: funcNameCandidate };
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ throw new Error(`Module not found: ${basePath}`);
116
+ }
117
+
118
+ /**
119
+ * Advanced Multi-File Dependency Extractor.
120
+ * Parses files, traces dependencies, and extracts everything mirroring the original structure.
121
+ */
122
+ class MultiFileExtractor {
123
+ /**
124
+ * @param {string} outDir - The base output directory.
125
+ */
126
+ constructor(outDir) {
127
+ this.outDir = outDir;
128
+ this.fileRequests = new Map();
129
+ this.processedFiles = new Map();
130
+ }
131
+
132
+ /**
133
+ * Registers a file and the specific functions needed from it into the extraction queue.
134
+ *
135
+ * @param {string} filePath - Absolute path to the file.
136
+ * @param {string[]} names - Specific exports to keep (empty array means full file).
137
+ * @param {string} versionDir - The base version directory for structural mirroring.
138
+ */
139
+ requestExtraction(filePath, names, versionDir) {
140
+ if (!this.fileRequests.has(filePath)) {
141
+ this.fileRequests.set(filePath, { names: new Set(), versionDir, full: false });
142
+ }
143
+ const req = this.fileRequests.get(filePath);
144
+
145
+ if (names.length === 0) {
146
+ req.full = true;
147
+ } else {
148
+ names.forEach((n) => req.names.add(n));
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Resolves local imports relative to the current file.
154
+ *
155
+ * @param {string} currentFilePath - The file importing the module.
156
+ * @param {string} importStr - The import string.
157
+ * @returns {string|null} Resolved absolute path or null if it's an external package.
158
+ */
159
+ resolveImportPath(currentFilePath, importStr) {
160
+ if (!importStr.startsWith('.')) return null; // Ignore external/node_modules
161
+
162
+ const dir = path.dirname(currentFilePath);
163
+ let resolved = path.resolve(dir, importStr);
164
+
165
+ if (!path.extname(resolved)) {
166
+ for (const ext of ['.mjs', '.js']) {
167
+ if (existsSync(resolved + ext)) return resolved + ext;
168
+ }
169
+ }
170
+ return resolved;
171
+ }
172
+
173
+ async processQueue() {
174
+ let processedCount = 0;
175
+
176
+ // Use a while loop because tracing dependencies will dynamically add new files to the Map
177
+ while (processedCount < this.fileRequests.size) {
178
+ const entries = Array.from(this.fileRequests.entries());
179
+
180
+ for (const [filePath, req] of entries) {
181
+ if (this.processedFiles.has(filePath)) continue;
182
+
183
+ const finalCode = await this.parseAndPrune(filePath, req);
184
+ this.processedFiles.set(filePath, { code: finalCode, versionDir: req.versionDir });
185
+ processedCount++;
186
+ }
187
+ }
188
+
189
+ await this.writeFiles();
190
+ }
191
+
192
+ /**
193
+ * Core AST manipulation: traces required imports and removes unused code.
194
+ *
195
+ * @param {string} filePath - Target file path.
196
+ * @param {Object} req - Request object containing required export names.
197
+ * @returns {Promise<string>} The minified and formatted generated source code.
198
+ */
199
+ async parseAndPrune(filePath, req) {
200
+ const sourceCode = await fs.readFile(filePath, 'utf-8');
201
+ const ast = parser.parse(sourceCode, { sourceType: 'module', plugins: ['classProperties'] });
202
+
203
+ const localKeepNames = new Set();
204
+ const requiredImports = new Map();
205
+
206
+ // Phase 1: Deep traceability
207
+ traverse(ast, {
208
+ Program: (astPath) => {
209
+ const bindings = astPath.scope.bindings;
210
+
211
+ if (req.full) {
212
+ Object.keys(bindings).forEach((name) => localKeepNames.add(name));
213
+ }
214
+
215
+ const queue = [];
216
+ const addToKeep = (name) => {
217
+ if (!localKeepNames.has(name)) {
218
+ localKeepNames.add(name);
219
+ queue.push(name);
220
+ }
221
+ };
222
+
223
+ astPath.traverse({
224
+ // Tracing Static Methods on any Class
225
+ ClassDeclaration(classPath) {
226
+ if (req.full) return;
227
+ classPath.node.body.body.forEach((item) => {
228
+ const keyName = t.isIdentifier(item.key) ? item.key.name : null;
229
+ if (
230
+ keyName &&
231
+ item.static &&
232
+ (t.isClassMethod(item) || t.isClassProperty(item)) &&
233
+ req.names.has(keyName)
234
+ ) {
235
+ traverse(
236
+ item,
237
+ {
238
+ Identifier(idPath) {
239
+ if (idPath.isReferencedIdentifier() && bindings[idPath.node.name]) {
240
+ addToKeep(idPath.node.name);
241
+ }
242
+ },
243
+ },
244
+ astPath.scope,
245
+ astPath,
246
+ );
247
+ }
248
+ });
249
+ },
250
+ CallExpression(callPath) {
251
+ if (callPath.node.callee.type === 'Import') {
252
+ const arg = callPath.node.arguments[0];
253
+ if (t.isStringLiteral(arg)) {
254
+ const source = arg.value;
255
+ if (!requiredImports.has(source)) requiredImports.set(source, new Set());
256
+ requiredImports.get(source).add({ local: '*', imported: '*' });
257
+ }
258
+ }
259
+ },
260
+ ImportDeclaration(expPath) {
261
+ const source = expPath.node.source.value;
262
+ if (!requiredImports.has(source)) requiredImports.set(source, new Set());
263
+
264
+ if (req.full) {
265
+ if (expPath.node.specifiers.length === 0) {
266
+ requiredImports.get(source).add({ local: null, imported: null });
267
+ } else {
268
+ expPath.node.specifiers.forEach((spec) => {
269
+ const importedName = t.isImportDefaultSpecifier(spec)
270
+ ? 'default'
271
+ : t.isImportNamespaceSpecifier(spec)
272
+ ? '*'
273
+ : spec.imported.name;
274
+ requiredImports
275
+ .get(source)
276
+ .add({ local: spec.local.name, imported: importedName });
277
+ });
278
+ }
279
+ }
280
+ },
281
+ // Catch re-exports (export * from './module')
282
+ ExportAllDeclaration(expPath) {
283
+ if (expPath.node.source) {
284
+ const source = expPath.node.source.value;
285
+ if (!requiredImports.has(source)) requiredImports.set(source, new Set());
286
+ // Always request '*' to trace export everything if needed
287
+ requiredImports.get(source).add({ local: '*', imported: '*', isExportAll: true });
288
+ }
289
+ },
290
+ ExportNamedDeclaration(expPath) {
291
+ if (expPath.node.source) {
292
+ // Tracing specific re-exports: export { target } from './module'
293
+ const source = expPath.node.source.value;
294
+ if (!requiredImports.has(source)) requiredImports.set(source, new Set());
295
+
296
+ expPath.node.specifiers.forEach((spec) => {
297
+ const exportedName = spec.exported.name;
298
+ if (req.full || req.names.has(exportedName)) {
299
+ const importedName = spec.local ? spec.local.name : exportedName;
300
+ requiredImports
301
+ .get(source)
302
+ .add({ local: null, imported: importedName, isReExport: true, exportedName });
303
+ }
304
+ });
305
+ } else {
306
+ const dec = expPath.node.declaration;
307
+ if (dec) {
308
+ if (t.isFunctionDeclaration(dec) && (req.full || req.names.has(dec.id.name))) {
309
+ addToKeep(dec.id.name);
310
+ } else if (t.isVariableDeclaration(dec)) {
311
+ dec.declarations.forEach((d) => {
312
+ if (t.isIdentifier(d.id) && (req.full || req.names.has(d.id.name)))
313
+ addToKeep(d.id.name);
314
+ });
315
+ }
316
+ } else if (expPath.node.specifiers) {
317
+ expPath.node.specifiers.forEach((spec) => {
318
+ if (req.full || req.names.has(spec.exported.name)) addToKeep(spec.local.name);
319
+ });
320
+ }
321
+ }
322
+ },
323
+ ExportDefaultDeclaration(expPath) {
324
+ const dec = expPath.node.declaration;
325
+ if (req.full || req.names.has('default')) {
326
+ if (t.isIdentifier(dec)) addToKeep(dec.name);
327
+ else if (dec.id) addToKeep(dec.id.name);
328
+ }
329
+ },
330
+ });
331
+
332
+ // Trace local variables and imports inside the queue
333
+ let i = 0;
334
+ while (i < queue.length) {
335
+ const currentName = queue[i++];
336
+ const binding = bindings[currentName];
337
+
338
+ if (binding) {
339
+ // Namespace Specifier Fix Included!
340
+ if (
341
+ binding.path.isImportSpecifier() ||
342
+ binding.path.isImportDefaultSpecifier() ||
343
+ binding.path.isImportNamespaceSpecifier()
344
+ ) {
345
+ const importDecl = binding.path.parentPath.node;
346
+ const source = importDecl.source.value;
347
+ if (!requiredImports.has(source)) requiredImports.set(source, new Set());
348
+
349
+ const importedName = binding.path.isImportDefaultSpecifier()
350
+ ? 'default'
351
+ : binding.path.isImportNamespaceSpecifier()
352
+ ? '*'
353
+ : binding.path.node.imported.name;
354
+ requiredImports.get(source).add({ local: currentName, imported: importedName });
355
+ } else {
356
+ binding.path.traverse({
357
+ Identifier(idPath) {
358
+ if (idPath.isReferencedIdentifier() && bindings[idPath.node.name]) {
359
+ addToKeep(idPath.node.name);
360
+ }
361
+ },
362
+ });
363
+ }
364
+ }
365
+ }
366
+ },
367
+ });
368
+
369
+ // Trigger extraction of all found deep dependencies
370
+ for (const [source, specs] of requiredImports.entries()) {
371
+ const resolvedPath = this.resolveImportPath(filePath, source);
372
+ if (resolvedPath) {
373
+ // If it's a full export dependency (like side-effect imports or export *), request full extraction
374
+ const isFullDependency = Array.from(specs).some(
375
+ (s) => s.imported === null || s.imported === '*',
376
+ );
377
+ const importedNamesToRequest = isFullDependency
378
+ ? []
379
+ : Array.from(specs).map((s) => s.imported);
380
+
381
+ this.requestExtraction(resolvedPath, importedNamesToRequest, req.versionDir);
382
+ }
383
+ }
384
+
385
+ // Phase 2: Surgical Pruning
386
+ if (!req.full) {
387
+ // Helper function to extract Class Methods AND Class Properties natively
388
+ const extractStaticMethods = (classNode) => {
389
+ const newNodes = [];
390
+ classNode.body.body.forEach((item) => {
391
+ const keyName = t.isIdentifier(item.key) ? item.key.name : null;
392
+ if (
393
+ keyName &&
394
+ item.static &&
395
+ (t.isClassMethod(item) || t.isClassProperty(item)) &&
396
+ req.names.has(keyName)
397
+ ) {
398
+ if (t.isClassMethod(item)) {
399
+ const func = t.functionDeclaration(
400
+ t.identifier(keyName),
401
+ item.params,
402
+ item.body,
403
+ item.generator,
404
+ item.async,
405
+ );
406
+ if (item.leadingComments) func.leadingComments = item.leadingComments;
407
+ newNodes.push(t.exportNamedDeclaration(func));
408
+ } else if (t.isClassProperty(item)) {
409
+ const variableDec = t.variableDeclaration('const', [
410
+ t.variableDeclarator(t.identifier(keyName), item.value),
411
+ ]);
412
+ if (item.leadingComments) variableDec.leadingComments = item.leadingComments;
413
+ newNodes.push(t.exportNamedDeclaration(variableDec));
414
+ }
415
+ }
416
+ });
417
+ return newNodes;
418
+ };
419
+
420
+ traverse(ast, {
421
+ Program(astPath) {
422
+ const body = astPath.get('body');
423
+ for (const statement of body) {
424
+ let keep = false;
425
+
426
+ if (statement.isImportDeclaration()) {
427
+ const source = statement.node.source.value;
428
+ if (requiredImports.has(source)) {
429
+ const neededSpecs = requiredImports.get(source);
430
+ const neededLocalNames = new Set(
431
+ Array.from(neededSpecs)
432
+ .map((s) => s.local)
433
+ .filter(Boolean),
434
+ );
435
+
436
+ if (statement.node.specifiers.length > 0) {
437
+ statement.node.specifiers = statement.node.specifiers.filter((spec) =>
438
+ neededLocalNames.has(spec.local.name),
439
+ );
440
+ if (statement.node.specifiers.length > 0) keep = true;
441
+ }
442
+ }
443
+ } else if (statement.isExportNamedDeclaration()) {
444
+ if (statement.node.source) {
445
+ const source = statement.node.source.value;
446
+ if (requiredImports.has(source)) {
447
+ const neededSpecs = requiredImports.get(source);
448
+ const neededExportedNames = new Set(
449
+ Array.from(neededSpecs)
450
+ .filter((s) => s.isReExport)
451
+ .map((s) => s.exportedName),
452
+ );
453
+
454
+ statement.node.specifiers = statement.node.specifiers.filter((spec) =>
455
+ neededExportedNames.has(spec.exported.name),
456
+ );
457
+ if (statement.node.specifiers.length > 0) keep = true;
458
+ }
459
+ } else {
460
+ const dec = statement.get('declaration');
461
+ if (
462
+ dec &&
463
+ dec.isFunctionDeclaration() &&
464
+ (req.names.has(dec.node.id.name) || localKeepNames.has(dec.node.id.name))
465
+ ) {
466
+ keep = true;
467
+ } else if (dec && dec.isVariableDeclaration()) {
468
+ const keepers = dec.node.declarations.filter(
469
+ (d) =>
470
+ t.isIdentifier(d.id) &&
471
+ (req.names.has(d.id.name) || localKeepNames.has(d.id.name)),
472
+ );
473
+ if (keepers.length > 0) {
474
+ dec.node.declarations = keepers;
475
+ keep = true;
476
+ }
477
+ } else if (dec && dec.isClassDeclaration()) {
478
+ if (req.names.has(dec.node.id.name) || localKeepNames.has(dec.node.id.name)) {
479
+ keep = true;
480
+ } else {
481
+ const extractedNodes = extractStaticMethods(dec.node);
482
+ if (extractedNodes.length > 0) {
483
+ statement.replaceWithMultiple(extractedNodes);
484
+ keep = true;
485
+ continue;
486
+ }
487
+ }
488
+ } else if (statement.node.specifiers && statement.node.specifiers.length > 0) {
489
+ statement.node.specifiers = statement.node.specifiers.filter(
490
+ (spec) =>
491
+ req.names.has(spec.exported.name) || localKeepNames.has(spec.local.name),
492
+ );
493
+ if (statement.node.specifiers.length > 0) keep = true;
494
+ }
495
+ }
496
+ } else if (statement.isExportAllDeclaration()) {
497
+ const source = statement.node.source.value;
498
+ if (requiredImports.has(source)) {
499
+ const neededSpecs = requiredImports.get(source);
500
+ if (Array.from(neededSpecs).some((s) => s.isExportAll)) keep = true;
501
+ }
502
+ } else if (statement.isExportDefaultDeclaration()) {
503
+ if (req.names.has('default')) {
504
+ keep = true;
505
+ } else if (t.isClassDeclaration(statement.node.declaration)) {
506
+ const extractedNodes = extractStaticMethods(statement.node.declaration);
507
+ if (extractedNodes.length > 0) {
508
+ statement.replaceWithMultiple(extractedNodes);
509
+ keep = true;
510
+ continue;
511
+ }
512
+ }
513
+ } else if (statement.isClassDeclaration()) {
514
+ if (localKeepNames.has(statement.node.id.name)) {
515
+ keep = true;
516
+ } else {
517
+ const extractedNodes = extractStaticMethods(statement.node);
518
+ if (extractedNodes.length > 0) {
519
+ statement.replaceWithMultiple(extractedNodes);
520
+ keep = true;
521
+ continue;
522
+ }
523
+ }
524
+ } else if (
525
+ statement.isFunctionDeclaration() &&
526
+ localKeepNames.has(statement.node.id.name)
527
+ ) {
528
+ keep = true;
529
+ } else if (statement.isVariableDeclaration()) {
530
+ const keepers = statement.node.declarations.filter(
531
+ (d) => t.isIdentifier(d.id) && localKeepNames.has(d.id.name),
532
+ );
533
+ if (keepers.length > 0) {
534
+ statement.node.declarations = keepers;
535
+ keep = true;
536
+ }
537
+ }
538
+
539
+ if (!keep) statement.remove();
540
+ }
541
+ },
542
+ });
543
+ }
544
+
545
+ return generate(ast).code;
546
+ }
547
+
548
+ /**
549
+ * Flushes processed files mapping onto the filesystem maintaining origin structure.
550
+ */
551
+ async writeFiles() {
552
+ let filesSaved = 0;
553
+ for (const [filePath, data] of this.processedFiles.entries()) {
554
+ const relativeToVersion = path.relative(data.versionDir, filePath);
555
+ const destPath = path.join(this.outDir, relativeToVersion);
556
+
557
+ await fs.mkdir(path.dirname(destPath), { recursive: true });
558
+ await fs.writeFile(destPath, data.code, 'utf-8');
559
+ console.log(`> Created: ${relativeToVersion}`);
560
+ filesSaved++;
561
+ }
562
+ console.log(`\nSuccessfully orchestrated and bundled ${filesSaved} isolated files!`);
563
+ }
564
+ }
565
+
566
+ /**
567
+ * Main execution function for the CLI tool.
568
+ */
569
+ async function runCLI() {
570
+ const args = process.argv.slice(2);
571
+
572
+ const outDirArg = args.find((a) => a.startsWith('--out-dir='));
573
+ const customOutDir = outDirArg ? outDirArg.split('=')[1] : null;
574
+
575
+ // Filter out flags to get the raw target strings
576
+ const rawTargets = args.filter((a) => !a.startsWith('--'));
577
+
578
+ if (rawTargets.length === 0) {
579
+ console.error('Usage: npx tiny-essentials-fork [--out-dir=custom/path] <target1> [target2] ...');
580
+ console.error('Example: npx tiny-essentials-fork v1/basics/example/func1 v1/lib/TinyHtml');
581
+ process.exit(1);
582
+ }
583
+
584
+ try {
585
+ const finalVendorDir = PATHS.getVendorDir(customOutDir);
586
+ const extractor = new MultiFileExtractor(finalVendorDir);
587
+
588
+ for (const rawTarget of rawTargets) {
589
+ let { targetVersion, basePath, functionsToExtract } = parseTarget(rawTarget);
590
+ const resolved = await resolveSourceFile(targetVersion, basePath);
591
+
592
+ if (resolved.interceptedFunction && functionsToExtract.length === 0) {
593
+ functionsToExtract.push(resolved.interceptedFunction);
594
+ }
595
+
596
+ extractor.requestExtraction(resolved.filePath, functionsToExtract, resolved.versionDir);
597
+ }
598
+
599
+ console.log(`Starting extraction to: ${finalVendorDir}\n`);
600
+ await extractor.processQueue();
601
+ } catch (error) {
602
+ console.error('\nError extracting modules:');
603
+ console.error(error.message);
604
+ process.exit(1);
605
+ }
606
+ }
607
+
608
+ runCLI();
@@ -0,0 +1,5 @@
1
+ # 🚀 Introducing TinyFork CLI: The Smart Extractor for Tiny-Essentials! 🍴✨
2
+
3
+ If you've ever wanted to use just a single function or class from `tiny-essentials` without bringing the entire library into your bundle, this release is for you! TinyFork is a custom, AST-powered tool that deeply scans our JavaScript modules and extracts *exactly* what you ask for.
4
+
5
+ It automatically traces and pulls in any required local dependencies, maintains the original `jsDoc` comments, and perfectly mirrors the original folder structure in your project.
@@ -0,0 +1,89 @@
1
+ # 🍴 TinyFork CLI (BETA)
2
+
3
+ **The smart, AST-powered tree-shaking extractor for `tiny-essentials`.**
4
+
5
+ Welcome to TinyFork! This is a friendly command-line interface (CLI) designed to extract specific functions, classes, and modules from the `tiny-essentials` library. Instead of forcing you to bundle the entire library into your project, TinyFork performs a deep Abstract Syntax Tree (AST) analysis to cherry-pick exactly what you need. It automatically resolves and extracts all necessary local dependencies, imports, and jsDocs without bringing in any dead code.
6
+
7
+ ### 📦 Prerequisites
8
+
9
+ Before running TinyFork, make sure you have the required Babel core packages installed in your project so the AST parsing can work its magic:
10
+ ```bash
11
+ npm install @babel/core @babel/parser @babel/traverse @babel/generator @babel/types
12
+ ```
13
+
14
+ ## ✨ Key Features
15
+
16
+ * 🌳 **Deep Tree-Shaking:** Uses `@babel/parser` to deeply trace your code. If you extract one function, TinyFork automatically finds and includes its dependencies (variables, other functions, dynamic imports) while leaving the rest behind.
17
+ * 📁 **Structure Mirroring:** Extracted files are automatically saved maintaining their original folder structure inside your project, preventing naming conflicts and keeping imports clean.
18
+ * 🧩 **Smart Class Extraction:** Can extract static methods from default exported classes and convert them into clean, standalone functions.
19
+ * 🔄 **Version Fallback:** Automatically scans for the requested files starting from the latest version down to `v1`. You can also explicitly request a specific version.
20
+
21
+ ---
22
+
23
+ ## 🚀 Usage
24
+
25
+ You can run TinyFork directly using `npx` from any project that has `tiny-essentials` installed.
26
+
27
+ **Basic Syntax:**
28
+ ```bash
29
+ npx tiny-essentials-fork [--out-dir=custom/path] <target1> [target2] ...
30
+ ```
31
+
32
+ By default, all files are extracted to: `./vendor/tiny-essentials/` at the root of your project.
33
+
34
+ ---
35
+
36
+ ## 📖 Examples & Capabilities
37
+
38
+ ### 1. Extracting a Full File
39
+ If you want to extract an entire JavaScript module, simply provide the path to the file (without the extension). TinyFork will extract it and automatically trace all of its internal imports!
40
+
41
+ ```bash
42
+ npx tiny-essentials-fork basics/array
43
+ ```
44
+ *Result:* Creates `vendor/tiny-essentials/basics/array.mjs` and includes any other internal modules that `array.mjs` depends on.
45
+
46
+ ### 2. Extracting Specific Functions
47
+ To extract only specific exports from a file, append them to the path separated by commas.
48
+
49
+ ```bash
50
+ npx tiny-essentials-fork basics/array/multiplyArrayBlocks,diffArrayList
51
+ ```
52
+ *Result:* Extracts **only** `multiplyArrayBlocks` and `diffArrayList` (and their specific local dependencies) from the `basics/array.mjs` file, discarding all other unused functions.
53
+
54
+ ### 3. Extracting Static Methods from a Class
55
+ If a module exports a default class filled with static methods, you can extract those methods just like regular functions. TinyFork will intelligently detach them from the class and export them as standalone functions!
56
+
57
+ ```bash
58
+ npx tiny-essentials-fork libs/TinyArrayComparator/generateHash
59
+ ```
60
+
61
+ ### 4. Extracting Multiple Targets at Once
62
+ You can queue as many targets as you want in a single command. TinyFork will orchestrate the entire extraction, mapping all dependencies together.
63
+
64
+ ```bash
65
+ npx tiny-essentials-fork libs/TinyHtml basics/array/multiplyArrayBlocks libs/TinyArrayComparator/generateHash
66
+ ```
67
+
68
+ ### 5. Specifying a Library Version
69
+ If you need a file from a specific version of `tiny-essentials` (e.g., `v1`), prepend the version number to the path. If no version is specified, TinyFork starts searching from the latest version downwards.
70
+
71
+ ```bash
72
+ npx tiny-essentials-fork v1/basics/array/multiplyArrayBlocks
73
+ ```
74
+
75
+ ### 6. Using a Custom Output Directory
76
+ Don't want to use the default `vendor/tiny-essentials` folder? Use the `--out-dir` flag to specify your own path.
77
+
78
+ ```bash
79
+ npx tiny-essentials-fork --out-dir=src/assets/tiny-modules basics/array
80
+ ```
81
+
82
+ ---
83
+
84
+ ## ⚙️ How it Works Under the Hood
85
+
86
+ 1. **Target Parsing:** TinyFork reads your CLI arguments and resolves the correct file path and version inside the `tiny-essentials/dist` folder.
87
+ 2. **Dependency Tracing (Bottom-Up):** Using Babel, it builds an AST of the requested file. It traces all identifiers used by your target functions, finding exactly which local variables and internal imports are required to make them work.
88
+ 3. **Surgical Pruning:** It removes all AST nodes (dead code) that are not part of the required trace.
89
+ 4. **Formatting & Output:** The optimized AST is generated back into standard JavaScript (preserving your original `jsDoc` comments) and written to your project's file system, perfectly mirroring the original directory structure.
package/docs/v1/README.md CHANGED
@@ -86,6 +86,14 @@ To get started, navigate to the appropriate directory and explore the files list
86
86
 
87
87
  ---
88
88
 
89
+ ## 🍴 TinyFork CLI
90
+
91
+ Want to extract only specific modules, classes, or functions into your project without carrying the whole library? We built a powerful AST-based CLI specifically for this!
92
+
93
+ 👉 Discover how to use our smart tree-shaking tool by visiting the [**TinyFork CLI Documentation**](../TinyFork.md) for important usage information and commands.
94
+
95
+ ---
96
+
89
97
  ## 📖 More Regex Goodies
90
98
 
91
99
  Looking for practical regex examples and migration helpers?
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "tiny-essentials",
3
- "version": "1.25.6",
3
+ "version": "1.26.0",
4
4
  "description": "Collection of small, essential scripts designed to be used across various projects. These simple utilities are crafted for speed, ease of use, and versatility.",
5
+ "bin": {
6
+ "tiny-essentials-fork": "./TinyFork.mjs"
7
+ },
5
8
  "scripts": {
6
9
  "test": "npm run test:mjs && npm run test:cjs && npm run test:js",
7
10
  "test:js": "npx babel-node test/index.js",
@@ -15,11 +18,12 @@
15
18
  "test:mjs:levelup": "node test/index.mjs levelUp",
16
19
  "test:mjs:filemanager": "node test/index.mjs fileManager",
17
20
  "test:mjs:i18": "node test/index.mjs i18",
18
- "fix:prettier": "npm run fix:prettier:src && npm run fix:prettier:test && npm run fix:prettier:rollup.config && npm run fix:prettier:webpack.config",
21
+ "fix:prettier": "npm run fix:prettier:src && npm run fix:prettier:test && npm run fix:prettier:rollup.config && npm run fix:prettier:webpack.config && npm run fix:prettier:tiny_fork",
19
22
  "fix:prettier:src": "prettier --write ./src/*",
20
23
  "fix:prettier:test": "prettier --write ./test/*",
21
24
  "fix:prettier:rollup.config": "prettier --write ./rollup.config.mjs",
22
25
  "fix:prettier:webpack.config": "prettier --write ./webpack.config.mjs",
26
+ "fix:prettier:tiny_fork": "prettier --write ./TinyFork.mjs",
23
27
  "auto-build": "npm run build",
24
28
  "build": "npm run build:js && npm run build:css",
25
29
  "build:js": "tsc -p tsconfig.json && rollup -c && webpack --mode production",
@@ -493,10 +497,14 @@
493
497
  },
494
498
  "homepage": "https://github.com/Tiny-Essentials/Tiny-Essentials#readme",
495
499
  "devDependencies": {
496
- "@babel/cli": "^7.27.0",
497
- "@babel/core": "^7.26.10",
498
- "@babel/node": "^7.26.0",
499
- "@babel/preset-env": "^7.26.9",
500
+ "@babel/cli": "^7.28.6",
501
+ "@babel/core": "^7.29.0",
502
+ "@babel/generator": "^7.29.1",
503
+ "@babel/node": "^7.29.0",
504
+ "@babel/parser": "^7.29.2",
505
+ "@babel/preset-env": "^7.29.2",
506
+ "@babel/traverse": "^7.29.0",
507
+ "@babel/types": "^7.29.0",
500
508
  "@rollup/plugin-commonjs": "^28.0.3",
501
509
  "@rollup/plugin-json": "^6.1.0",
502
510
  "@rollup/plugin-node-resolve": "^16.0.1",