prodex 1.0.8 → 1.1.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.
@@ -1,122 +1,116 @@
1
1
  import fs from "fs";
2
- import path from "path";
3
2
  import inquirer from "inquirer";
4
- import {
5
- ROOT,
6
- CODE_EXTS,
7
- RESOLVERS,
8
- PROMPTS
9
- } from "../constants/config.js";
10
- import { loadProdexConfig } from "../constants/config-loader.js";
11
- import { read, normalizeIndent, stripComments, rel } from "./helpers.js";
3
+ import path from "path";
4
+ import micromatch from "micromatch";
12
5
  import { pickEntries } from "../cli/picker.js";
13
6
  import { showSummary } from "../cli/summary.js";
14
- import { generateOutputName, resolveOutputPath } from "./file-utils.js";
7
+ import { loadProdexConfig } from "../constants/config-loader.js";
8
+ import { CODE_EXTS, RESOLVERS, ROOT } from "../constants/config.js";
9
+ import { generateOutputName, resolveOutDirPath, safeMicromatchScan } from "./file-utils.js";
10
+ import { renderMd, renderTxt, tocMd, tocTxt } from "./renderers.js";
11
+
15
12
 
16
- export async function runCombine() {
13
+ export async function runCombine(opts = {}) {
17
14
  const cliLimitFlag = process.argv.find(arg => arg.startsWith("--limit="));
18
15
  const customLimit = cliLimitFlag ? parseInt(cliLimitFlag.split("=")[1], 10) : null;
16
+ const cliTxtFlag = process.argv.includes("--txt");
19
17
 
20
18
  const cfg = loadProdexConfig();
21
- const { baseDirs, scanDepth } = cfg;
19
+ const { scanDepth } = cfg;
20
+
21
+ let entries = opts.entries;
22
+
23
+ // 🧩 Headless mode: expand globs manually
24
+ if (entries && entries.length) {
25
+ const all = [];
26
+ for (const pattern of entries) {
27
+ const abs = path.resolve(process.cwd(), pattern);
28
+ if (fs.existsSync(abs) && fs.statSync(abs).isFile()) {
29
+ // direct file path (no glob)
30
+ all.push(abs);
31
+ continue;
32
+ }
33
+
34
+ // glob pattern
35
+ const result = safeMicromatchScan(pattern, {
36
+ cwd: process.cwd(),
37
+ absolute: true,
38
+ });
39
+ if (result?.files?.length) all.push(...result.files);
40
+ }
41
+ entries = [...new Set(all)];
42
+ } else {
43
+ // fallback to interactive picker
44
+ entries = await pickEntries(cfg.entry.includes, scanDepth, cfg);
45
+ }
46
+
22
47
 
23
- const entries = await pickEntries(baseDirs, scanDepth, cfg);
24
48
  if (!entries.length) {
25
49
  console.log("āŒ No entries selected.");
26
50
  return;
27
51
  }
28
52
 
29
- const autoName = generateOutputName(entries);
30
- const outputDir = cfg.output || path.join(ROOT, "prodex");
31
- const defaultLimit = customLimit || cfg.limit || 200;
32
-
33
53
  console.log("\nšŸ“‹ You selected:");
34
- for (const e of entries) console.log(" -", rel(e));
35
-
36
- const { yesToAll } = await inquirer.prompt([PROMPTS.yesToAll]);
37
-
38
- let outputBase = autoName,
39
- limit = defaultLimit,
40
- chain = true,
41
- proceed = true;
42
-
43
- if (!yesToAll) {
44
- const combinePrompts = PROMPTS.combine.map(p => ({
45
- ...p,
46
- default:
47
- p.name === "outputBase"
48
- ? autoName
49
- : p.name === "limit"
50
- ? defaultLimit
51
- : p.default
52
- }));
53
-
54
- const ans = await inquirer.prompt(combinePrompts);
55
- outputBase = ans.outputBase || autoName;
56
- limit = ans.limit;
57
- chain = ans.chain;
58
- proceed = ans.proceed;
59
- }
54
+ for (const e of entries) console.log(" -", e.replace(ROOT + "/", ""));
60
55
 
61
- if (!proceed) {
62
- console.log("āš™ļø Aborted.");
63
- return;
56
+ // 🧩 Auto name suggestion
57
+ const autoName = generateOutputName(entries);
58
+ const outDir = cfg.outDir || path.join(ROOT, "prodex");
59
+ const limit = customLimit || cfg.limit || 200;
60
+ const chain = true;
61
+
62
+ // Skip prompt if entries were passed directly
63
+ let outputBase = autoName;
64
+ if (!opts.entries?.length) {
65
+ const { outputBase: answer } = await inquirer.prompt([
66
+ {
67
+ type: "input",
68
+ name: "outputBase",
69
+ message: "Output file name (without extension):",
70
+ default: autoName,
71
+ filter: v => (v.trim() || autoName).replace(/[<>:"/\\|?*]+/g, "_"),
72
+ },
73
+ ]);
74
+ outputBase = answer;
64
75
  }
65
76
 
77
+ // Ensure output directory exists
66
78
  try {
67
- fs.mkdirSync(outputDir, { recursive: true });
79
+ fs.mkdirSync(outDir, { recursive: true });
68
80
  } catch {
69
- console.warn("āš ļø Could not create output directory:", outputDir);
81
+ console.warn("āš ļø Could not create outDir directory:", outDir);
70
82
  }
71
83
 
72
- const output = resolveOutputPath(outputDir, outputBase);
84
+ const outputPath = resolveOutDirPath(outDir, outputBase, cliTxtFlag);
73
85
 
74
- showSummary({
75
- outputDir,
76
- fileName: path.basename(output),
77
- entries,
78
- scanDepth: cfg.scanDepth,
79
- limit,
80
- chain
81
- });
86
+ showSummary({ outDir, fileName: path.basename(outputPath), entries });
82
87
 
83
- const finalFiles = chain ? await followChain(entries, limit) : entries;
84
88
 
85
- fs.writeFileSync(
86
- output,
87
- [toc(finalFiles), ...finalFiles.map(render)].join(""),
88
- "utf8"
89
- );
89
+ const result = chain ? await followChain(entries, cfg, limit) : { files: entries, stats: { totalImports: 0, totalResolved: 0 } };
90
+ const sorted = [...result.files].sort((a, b) => a.localeCompare(b));
90
91
 
91
- console.log(`\nāœ… ${output} written (${finalFiles.length} file(s)).`);
92
- }
92
+ const content = cliTxtFlag
93
+ ? [tocTxt(sorted), ...sorted.map(renderTxt)].join("")
94
+ : [tocMd(sorted), ...sorted.map((f, i) => renderMd(f, i === 0))].join("\n");
93
95
 
94
- function header(p) {
95
- return `##==== path: ${rel(p)} ====`;
96
- }
97
- function regionStart(p) {
98
- return `##region ${rel(p)}`;
99
- }
100
- const regionEnd = "##endregion";
101
-
102
- function render(p) {
103
- const ext = path.extname(p);
104
- let s = read(p);
105
- return `${header(p)}\n${regionStart(p)}\n${s}\n${regionEnd}\n\n`;
106
- }
107
-
108
- function toc(files) {
109
- return (
110
- ["##==== Combined Scope ====", ...files.map(f => "## - " + rel(f))].join(
111
- "\n"
112
- ) + "\n\n"
96
+ fs.writeFileSync(outputPath, content, "utf8");
97
+ console.log(
98
+ `\nāœ… ${outputPath}`
113
99
  );
100
+ // 🧩 Print resolver summary (clean version)
101
+ console.log(`\n🧩 Summary:
102
+ • Unique imports expected: ${result.stats.expected.size}
103
+ • Unique imports resolved: ${result.stats.resolved.size}
104
+ `);
114
105
  }
115
106
 
116
- async function followChain(entryFiles, limit = 200) {
107
+ async function followChain(entryFiles, cfg, limit = 200) {
117
108
  console.log("🧩 Following dependency chain...");
118
109
  const visited = new Set();
119
110
  const all = [];
111
+ const expected = new Set();
112
+ const resolved = new Set();
113
+ const resolverDepth = cfg.resolverDepth ?? 10;
120
114
 
121
115
  for (const f of entryFiles) {
122
116
  if (visited.has(f)) continue;
@@ -127,15 +121,25 @@ async function followChain(entryFiles, limit = 200) {
127
121
 
128
122
  const resolver = RESOLVERS[ext];
129
123
  if (resolver) {
130
- const { files } = await resolver(f, visited);
124
+ const result = await resolver(f, cfg, visited, 0, resolverDepth);
125
+ const { files, stats } = result;
131
126
  all.push(...files);
127
+ stats?.expected?.forEach(x => expected.add(x));
128
+ stats?.resolved?.forEach(x => resolved.add(x));
132
129
  }
133
130
 
134
- if (all.length >= limit) {
131
+ if (limit && all.length >= limit) {
135
132
  console.log("āš ļø Limit reached:", limit);
136
133
  break;
137
134
  }
138
135
  }
139
136
 
140
- return [...new Set(all)];
137
+ return {
138
+ files: [...new Set(all)],
139
+ stats: {
140
+ expected,
141
+ resolved
142
+ }
143
+ };
141
144
  }
145
+
@@ -1,4 +1,35 @@
1
+ import fs from "fs";
1
2
  import path from "path";
3
+ import micromatch from "micromatch";
4
+
5
+ /**
6
+ * Safe micromatch.scan wrapper (compatible with micromatch v4 & v5)
7
+ */
8
+ export function safeMicromatchScan(pattern, opts = {}) {
9
+ const scanFn = micromatch.scan;
10
+ if (typeof scanFn === "function") return scanFn(pattern, opts);
11
+
12
+ // --- fallback for micromatch v4 ---
13
+ const cwd = opts.cwd || process.cwd();
14
+ const abs = !!opts.absolute;
15
+ const allFiles = listAllFiles(cwd);
16
+ const matched = micromatch.match(allFiles, pattern, { dot: true });
17
+ return { files: abs ? matched.map(f => path.resolve(cwd, f)) : matched };
18
+ }
19
+
20
+ /**
21
+ * Recursively list all files in a directory.
22
+ * Used only for fallback (so performance isn’t critical).
23
+ */
24
+ function listAllFiles(dir) {
25
+ const out = [];
26
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
27
+ const full = path.join(dir, entry.name);
28
+ if (entry.isDirectory()) out.push(...listAllFiles(full));
29
+ else out.push(full);
30
+ }
31
+ return out;
32
+ }
2
33
 
3
34
  export function generateOutputName(entries) {
4
35
  const names = entries.map(f => path.basename(f, path.extname(f)));
@@ -8,6 +39,7 @@ export function generateOutputName(entries) {
8
39
  return "unknown";
9
40
  }
10
41
 
11
- export function resolveOutputPath(outputDir, base) {
12
- return path.join(outputDir, `prodex-${base}-combined.txt`);
42
+ export function resolveOutDirPath(outDir, base, asTxt = false) {
43
+ const ext = asTxt ? "txt" : "md";
44
+ return path.join(outDir, `${base}-combined.${ext}`);
13
45
  }
@@ -1,11 +1,17 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import { ROOT, CODE_EXTS, ENTRY_EXCLUDES } from "../constants/config.js";
3
+ import micromatch from "micromatch";
4
4
 
5
- export function rel(p) {
6
- return path.relative(ROOT, p).replaceAll("\\", "/");
5
+ /**
6
+ * Get a root-relative version of a path.
7
+ */
8
+ export function rel(p, root = process.cwd()) {
9
+ return path.relative(root, p).replaceAll("\\", "/");
7
10
  }
8
11
 
12
+ /**
13
+ * Safe text read.
14
+ */
9
15
  export function read(p) {
10
16
  try {
11
17
  return fs.readFileSync(p, "utf8");
@@ -14,104 +20,47 @@ export function read(p) {
14
20
  }
15
21
  }
16
22
 
17
- export function normalizeIndent(s) {
18
- return s
19
- .replace(/\t/g, " ")
20
- .split("\n")
21
- .map(l => l.replace(/[ \t]+$/, ""))
22
- .join("\n");
23
+ /**
24
+ * Check if a path/file matches any of the provided glob patterns.
25
+ */
26
+ export function isExcluded(p, patterns, root = process.cwd()) {
27
+ if (!patterns?.length) return false;
28
+ const relPath = rel(p, root);
29
+ return micromatch.isMatch(relPath, patterns);
23
30
  }
24
31
 
25
- export function stripComments(code, ext) {
26
- if (ext === ".php") {
27
- return code
28
- .replace(/\/\*[\s\S]*?\*\//g, "")
29
- .replace(/^\s*#.*$/gm, "");
30
- }
31
-
32
- let out = "";
33
- let inStr = false;
34
- let strChar = "";
35
- let inBlockComment = false;
36
- let inLineComment = false;
37
-
38
- for (let i = 0; i < code.length; i++) {
39
- const c = code[i];
40
- const next = code[i + 1];
41
-
42
- if (inBlockComment) {
43
- if (c === "*" && next === "/") {
44
- inBlockComment = false;
45
- i++;
46
- }
47
- continue;
48
- }
49
-
50
- if (inLineComment) {
51
- if (c === "\n") {
52
- inLineComment = false;
53
- out += c;
54
- }
55
- continue;
56
- }
57
-
58
- if (inStr) {
59
- if (c === "\\" && next) {
60
- out += c + next;
61
- i++;
62
- continue;
63
- }
64
- if (c === strChar) inStr = false;
65
- out += c;
66
- continue;
67
- }
68
-
69
- if (c === '"' || c === "'" || c === "`") {
70
- inStr = true;
71
- strChar = c;
72
- out += c;
73
- continue;
74
- }
32
+ /**
33
+ * Recursive walker that respects glob excludes.
34
+ * Returns all files under the given directory tree.
35
+ */
36
+ export function* walk(dir, cfg, depth = 0) {
37
+ const { scanDepth, entry } = cfg;
38
+ const root = process.cwd();
39
+ if (depth > scanDepth) return;
75
40
 
76
- if (c === "/" && next === "*") {
77
- inBlockComment = true;
78
- i++;
79
- continue;
80
- }
41
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
42
+ for (const e of entries) {
43
+ const full = path.join(dir, e.name);
81
44
 
82
- if (c === "/" && next === "/") {
83
- inLineComment = true;
84
- i++;
45
+ if (e.isDirectory()) {
46
+ // Skip excluded directories entirely
47
+ const relPath = rel(full, root);
48
+ if (isExcluded(relPath, entry.excludes)) continue;
49
+ yield* walk(full, cfg, depth + 1);
85
50
  continue;
86
51
  }
87
52
 
88
- out += c;
89
- }
90
-
91
- return out;
92
- }
93
-
94
- export function isEntryExcluded(p) {
95
- const r = rel(p);
96
- return ENTRY_EXCLUDES.some(ex => r.startsWith(ex) || r.includes(ex));
97
- }
98
-
99
- export function* walk(dir, depth = 0, maxDepth = 2) {
100
- if (depth > maxDepth) return;
101
- const entries = fs.readdirSync(dir, { withFileTypes: true });
102
- for (const e of entries) {
103
- const full = path.join(dir, e.name);
104
- if (e.isDirectory()) yield* walk(full, depth + 1, maxDepth);
105
- else if (e.isFile()) {
106
- const ext = path.extname(e.name).toLowerCase();
107
- const relPath = rel(full);
108
- if (CODE_EXTS.includes(ext) && !ENTRY_EXCLUDES.some(ex => relPath.startsWith(ex))) {
109
- yield full;
110
- }
53
+ if (e.isFile()) {
54
+ const relPath = rel(full, root);
55
+ if (isExcluded(relPath, entry.excludes)) continue;
56
+ yield full;
111
57
  }
112
58
  }
113
59
  }
114
60
 
61
+ /**
62
+ * Sorts files so that priority items appear first.
63
+ */
115
64
  export function sortWithPriority(files, priorityList = []) {
116
65
  if (!priorityList.length) return files;
117
66
  const prioritized = [];
@@ -119,7 +68,8 @@ export function sortWithPriority(files, priorityList = []) {
119
68
 
120
69
  for (const f of files) {
121
70
  const normalized = f.replaceAll("\\", "/").toLowerCase();
122
- if (priorityList.some(p => normalized.includes(p.toLowerCase()))) prioritized.push(f);
71
+ if (priorityList.some(p => micromatch.isMatch(normalized, p.toLowerCase())))
72
+ prioritized.push(f);
123
73
  else normal.push(f);
124
74
  }
125
75
 
@@ -0,0 +1,58 @@
1
+ import path from "path";
2
+ import { read, rel } from "./helpers.js";
3
+ import { LANG_MAP } from "../constants/render-constants.js";
4
+
5
+ /**
6
+ * Generate Markdown Table of Contents
7
+ * Sorted alphabetically for deterministic structure.
8
+ */
9
+ export function tocMd(files) {
10
+ const sorted = [...files].sort((a, b) => a.localeCompare(b));
11
+ const items = sorted.map(f => "- " + rel(f)).join("\n");
12
+ return `# Included Source Files\n\n${items}\n\n---\n`;
13
+ }
14
+
15
+ /**
16
+ * Render a single file section in Markdown format.
17
+ * The first file skips the leading separator to avoid duplicates.
18
+ */
19
+ export function renderMd(p, isFirst = false) {
20
+ const rp = rel(p);
21
+ const ext = path.extname(p).toLowerCase();
22
+ const lang = LANG_MAP[ext] || "txt";
23
+ const code = read(p).trimEnd();
24
+
25
+ return [
26
+ isFirst ? "" : "---", // only add separator *after* the first file
27
+ `\`File: ${rp}\``,
28
+ "",
29
+ "```" + lang,
30
+ code,
31
+ "```",
32
+ ""
33
+ ]
34
+ .filter(Boolean)
35
+ .join("\n");
36
+ }
37
+
38
+ /**
39
+ * TXT version (unchanged)
40
+ */
41
+ export function tocTxt(files) {
42
+ const sorted = [...files].sort((a, b) => a.localeCompare(b));
43
+ return (
44
+ ["##==== Combined Scope ====", ...sorted.map(f => "## - " + rel(f))].join("\n") + "\n\n"
45
+ );
46
+ }
47
+
48
+ export function renderTxt(p) {
49
+ const relPath = rel(p);
50
+ const code = read(p);
51
+ return [
52
+ "##==== path: " + relPath + " ====",
53
+ "##region " + relPath,
54
+ code,
55
+ "##endregion",
56
+ ""
57
+ ].join("\n");
58
+ }
package/dist/src/index.js CHANGED
@@ -5,7 +5,11 @@ export default async function startProdex() {
5
5
  const args = process.argv.slice(2);
6
6
  if (args.includes("init")) return await initProdex();
7
7
 
8
+ const entryArgs = args.filter(a => !a.startsWith("--"));
9
+ const hasEntries = entryArgs.length > 0;
10
+
8
11
  console.clear();
9
12
  console.log("🧩 Prodex — Project Dependency Extractor\n");
10
- await runCombine();
13
+
14
+ await runCombine({ entries: hasEntries ? entryArgs : null });
11
15
  }