prodex 1.4.6 → 1.4.8

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.
@@ -9,6 +9,27 @@ const path_1 = __importDefault(require("path"));
9
9
  const package_json_1 = __importDefault(require("../../package.json"));
10
10
  const fs_1 = __importDefault(require("fs"));
11
11
  const flags_1 = require("../constants/flags");
12
+ function extractShortcutTokens(argv) {
13
+ const cleaned = [];
14
+ const shortcuts = [];
15
+ let shortcutAll = false;
16
+ for (const arg of argv) {
17
+ if (arg === "@") {
18
+ shortcutAll = true;
19
+ continue;
20
+ }
21
+ if (arg?.startsWith("@")) {
22
+ const name = arg.slice(1).trim();
23
+ if (!name)
24
+ shortcutAll = true;
25
+ else
26
+ shortcuts.push(name);
27
+ continue;
28
+ }
29
+ cleaned.push(arg);
30
+ }
31
+ return { argv: cleaned, shortcutAll, shortcuts };
32
+ }
12
33
  /**
13
34
  * Unified CLI parser powered by Sade and FLAG_MAP.
14
35
  * Returns { root, flags, warnings, errors }.
@@ -18,23 +39,34 @@ function parseCliInput(argv = process.argv) {
18
39
  console.log(`prodex v${package_json_1.default.version}`);
19
40
  process.exit(0);
20
41
  }
42
+ const extracted = extractShortcutTokens(argv);
43
+ const argvCleaned = extracted.argv;
21
44
  const program = (0, sade_1.default)("prodex [root]");
22
45
  registerFlags(program);
23
46
  let parsed = { rootArg: "", root: undefined, flags: {} };
24
47
  program.action((root, opts) => {
25
- let lroot = root;
26
48
  const cwd = process.cwd();
27
- if (root?.startsWith("@")) {
28
- opts.shortcut = root.slice(1).trim();
29
- lroot = undefined;
30
- }
31
49
  parsed = {
32
- rootArg: lroot,
33
- root: lroot ? path_1.default.resolve(cwd, lroot) : cwd,
50
+ rootArg: root,
51
+ root: root ? path_1.default.resolve(cwd, root) : cwd,
34
52
  flags: { ...opts },
35
53
  };
36
54
  });
37
- program.parse(argv);
55
+ program.parse(argvCleaned);
56
+ // Merge shortcut tokens (@a @b @c / @) with existing --shortcut usage.
57
+ const fromTokens = extracted.shortcuts;
58
+ const shortcutAll = extracted.shortcutAll;
59
+ const fromFlag = typeof parsed.flags.shortcut === "string" ? parsed.flags.shortcut.trim() : "";
60
+ const selected = [...fromTokens, ...(fromFlag ? [fromFlag] : [])].filter(Boolean);
61
+ const uniq = Array.from(new Set(selected));
62
+ if (shortcutAll)
63
+ parsed.flags.shortcutAll = true;
64
+ if (uniq.length)
65
+ parsed.flags.shortcuts = uniq;
66
+ if (!shortcutAll && uniq.length === 1)
67
+ parsed.flags.shortcut = uniq[0];
68
+ else if (uniq.length > 1 || shortcutAll)
69
+ delete parsed.flags.shortcut;
38
70
  const warnings = [];
39
71
  const errors = [];
40
72
  parsed.flags = normalizeFlags(parsed.flags, warnings, errors);
@@ -43,14 +75,12 @@ function parseCliInput(argv = process.argv) {
43
75
  }
44
76
  function registerFlags(program) {
45
77
  for (const [key, meta] of Object.entries(flags_1.FLAG_MAP)) {
46
- const short = meta.short ? `-${meta.short}, ` : "";
47
- const long = `--${key}`;
48
- const desc = meta.description;
49
- const defaultVal = meta.type === "boolean" ? false : undefined;
50
- program.option(`${short}${long}`, desc, defaultVal);
78
+ //@ts-ignore
79
+ const alias = meta.alias ? `-${meta.alias},` : "";
80
+ //@ts-ignore
81
+ program.option(`${alias}--${key}`, meta.desc);
51
82
  }
52
83
  }
53
- /** Convert flag values to correct types based on FLAG_MAP metadata. */
54
84
  function normalizeFlags(flags, warnings, errors) {
55
85
  for (const [key, meta] of Object.entries(flags_1.FLAG_MAP)) {
56
86
  const raw = flags[key];
@@ -72,6 +102,10 @@ function normalizeFlags(flags, warnings, errors) {
72
102
  .filter(Boolean);
73
103
  break;
74
104
  }
105
+ case "boolean": {
106
+ flags[key] = Boolean(raw);
107
+ break;
108
+ }
75
109
  default: {
76
110
  if (meta.type === "string")
77
111
  flags[key] = String(raw);
@@ -85,7 +119,7 @@ function validateArgs(parsed, warnings, errors) {
85
119
  const { rootArg } = parsed;
86
120
  if (rootArg) {
87
121
  if (!fs_1.default.existsSync(parsed.root)) {
88
- errors.push(`Invalid path argument: "${rootArg}" does not exist.`);
122
+ errors.push(`Invalid path "${rootArg}"`);
89
123
  }
90
124
  else if (!fs_1.default.statSync(parsed.root).isDirectory()) {
91
125
  errors.push(`Path argument "${rootArg}" is not a directory.`);
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.MD_FOOTER = exports.TEXT_HEADERS = exports.LANG_MAP = void 0;
6
+ exports.MD_HEADER = exports.MD_FOOTER = exports.TEXT_HEADERS = exports.LANG_MAP = void 0;
7
7
  const package_json_1 = __importDefault(require("../../package.json"));
8
8
  exports.LANG_MAP = {
9
9
  "": "js",
@@ -22,3 +22,4 @@ exports.TEXT_HEADERS = {
22
22
  regionEnd: "##endregion",
23
23
  };
24
24
  exports.MD_FOOTER = ["\n---", "*Generated with [Prodex](https://github.com/emxhive/prodex) — Codebase decoded.*", `<!-- PRODEx v${package_json_1.default.version} | ${new Date().toISOString()} -->`].join("\n");
25
+ exports.MD_HEADER = "*Generated by [Prodex](https://github.com/emxhive/prodex#readme)*";
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runCombine = runCombine;
4
4
  const picker_1 = require("../cli/picker");
5
5
  const summary_1 = require("../cli/summary");
6
- const cache_keys_1 = require("../constants/cache-keys");
6
+ const constants_1 = require("../constants");
7
7
  const cache_1 = require("./managers/cache");
8
8
  const config_1 = require("./managers/config");
9
9
  const logger_1 = require("../lib/logger");
@@ -49,7 +49,7 @@ async function resolveEntries(showUi, cfg) {
49
49
  * 🧩 Persist discovered aliases (if any)
50
50
  */
51
51
  function persistAliases() {
52
- const aliases = cache_1.CacheManager.dump(cache_keys_1.CACHE_KEYS.ALIASES);
52
+ const aliases = cache_1.CacheManager.dump(constants_1.CACHE_KEYS.ALIASES);
53
53
  if (Object.keys(aliases).length) {
54
54
  config_1.ConfigManager.persist({ resolve: { aliases } });
55
55
  }
@@ -10,6 +10,7 @@ const config_1 = require("../constants/config");
10
10
  const helpers_1 = require("./helpers");
11
11
  const logger_1 = require("../lib/logger");
12
12
  const collections_1 = require("../shared/collections");
13
+ const fs_1 = __importDefault(require("fs"));
13
14
  /**
14
15
  * 🧩 followChain()
15
16
  * Traverses all dependencies starting from the given entry files.
@@ -67,9 +68,31 @@ async function followChain(entryFiles, cfg) {
67
68
  * 🧩 applyIncludes()
68
69
  * Scans and appends additional files defined in config.resolve.include.
69
70
  */
71
+ // src/core/dependency.ts
72
+ // (existing imports stay)
70
73
  async function applyIncludes(cfg, files) {
71
74
  const { resolve, root } = cfg;
72
- const scan = await (0, helpers_1.globScan)(resolve.include, { cwd: root });
73
- logger_1.logger.debug("APPLY_include", _2j(scan));
74
- return (0, collections_1.unique)([...files, ...scan.files]);
75
+ const absFiles = [];
76
+ const patterns = [];
77
+ for (const raw of resolve.include) {
78
+ const p = String(raw ?? "").trim();
79
+ if (!p)
80
+ continue;
81
+ const norm = p.norm(); // uses your polyfill (slashes -> "/")
82
+ // absolute *file* paths bypass globScan (and its ignores)
83
+ if (path_1.default.isAbsolute(norm)) {
84
+ try {
85
+ if (fs_1.default.statSync(norm).isFile()) {
86
+ absFiles.push(path_1.default.resolve(norm));
87
+ continue;
88
+ }
89
+ }
90
+ catch {
91
+ // doesn't exist / can't stat → treat as pattern
92
+ }
93
+ }
94
+ patterns.push(norm);
95
+ }
96
+ const scan = await (0, helpers_1.globScan)(patterns, { cwd: root });
97
+ return (0, collections_1.unique)([...files, ...absFiles, ...scan.files]);
75
98
  }
@@ -21,7 +21,7 @@ class CacheManager {
21
21
  /** Set or update a cached entry */
22
22
  static set(ns, key, val) {
23
23
  this.ns(ns).set(key, val);
24
- logger_1.logger.debug(`🧩 [cache:${ns}] set ${key} → ${String(val)}`);
24
+ logger_1.logger.debug(`🧩 [cache:${ns}] set ${key} \n→ ${_2j(val)}`);
25
25
  }
26
26
  /** Retrieve a cached entry */
27
27
  static get(ns, key) {
@@ -58,7 +58,10 @@ class ConfigManager {
58
58
  def.apply(cfg, val);
59
59
  }
60
60
  const hasFiles = Array.isArray(flags.files) ? flags.files.length > 0 : !!flags.files;
61
- if (hasFiles && !flags.include)
61
+ const hasInclude = Array.isArray(flags.include) ? flags.include.length > 0 : !!flags.include;
62
+ if (hasInclude && !hasFiles)
63
+ cfg.entry.files = [];
64
+ if (hasFiles && !hasInclude)
62
65
  cfg.resolve.include = [];
63
66
  if (flags.shortcut && cfg.shortcuts && cfg.shortcuts[flags.shortcut])
64
67
  return this.applyShortcuts(cfg, flags);
@@ -11,7 +11,6 @@ const renderers_1 = require("./renderers");
11
11
  const logger_1 = require("../lib/logger");
12
12
  const utils_1 = require("../lib/utils");
13
13
  const questions_1 = require("../lib/questions");
14
- const render_constants_1 = require("../constants/render-constants");
15
14
  const config_1 = require("../constants/config");
16
15
  /**
17
16
  * 🧩 produceOutput()
@@ -20,7 +19,6 @@ const config_1 = require("../constants/config");
20
19
  */
21
20
  async function produceOutput({ name, files, cfg, showUi }) {
22
21
  const { output: { format, versioned, dir }, } = cfg;
23
- ;
24
22
  // 1️⃣ Determine base filename
25
23
  let outputBase = name;
26
24
  if (showUi) {
@@ -42,7 +40,10 @@ async function produceOutput({ name, files, cfg, showUi }) {
42
40
  // 4️⃣ Prepare and write content
43
41
  const outputPath = path_1.default.join(dir, `${outputBase}.${format}`);
44
42
  const sorted = [...files].sort((a, b) => a.localeCompare(b));
45
- const content = format === "txt" ? [(0, renderers_1.tocTxt)(sorted), ...sorted.map(renderers_1.renderTxt)].join("") : [(0, renderers_1.tocMd)(sorted), ...sorted.map((f, i) => (0, renderers_1.renderMd)(f, i)), render_constants_1.MD_FOOTER].join("\n");
43
+ //[tocMd(sorted), ...sorted.map((f, i) => renderMd(f, i)), MD_FOOTER].join("\n")
44
+ const content = format === "txt"
45
+ ? [(0, renderers_1.tocTxt)(sorted), ...sorted.map(renderers_1.renderTxt)].join("")
46
+ : (0, renderers_1.renderTraceMd)(sorted).content;
46
47
  fs_1.default.writeFileSync(outputPath, content, "utf8");
47
48
  return outputPath;
48
49
  }
@@ -3,41 +3,174 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.renderTraceMd = renderTraceMd;
6
7
  exports.tocMd = tocMd;
7
8
  exports.renderMd = renderMd;
8
9
  exports.tocTxt = tocTxt;
9
10
  exports.renderTxt = renderTxt;
10
11
  const path_1 = __importDefault(require("path"));
11
- const io_1 = require("../shared/io");
12
- const io_2 = require("../shared/io");
13
- const render_constants_1 = require("../constants/render-constants");
12
+ const shared_1 = require("../shared");
13
+ const shared_2 = require("../shared");
14
+ const constants_1 = require("../constants");
14
15
  /**
15
- * Generate Markdown Table of Contents with anchors
16
+ * renderTraceMd()
17
+ * Builds the full markdown document AND computes:
18
+ * - listing start/end lines
19
+ * - each file section start/end lines (in the final output)
16
20
  */
17
- function tocMd(files) {
21
+ // src/core/renderers.ts
22
+ function renderTraceMd(files) {
18
23
  const count = files.length;
19
- const items = files.map((f, i) => `- [${(0, io_2.rel)(f)}](#${i + 1})`).join("\n");
20
- return ["# Index ", `\nIncluded Source Files (${count})`, items, "", "---"].join("\n");
24
+ // Render sections once (content of each file)
25
+ const sections = files.map((f, i) => renderMd(f, i));
26
+ // 1) Build a FIRST PASS doc with placeholder TOC (no line ranges)
27
+ const pass1Toc = buildToc({
28
+ files,
29
+ count,
30
+ listingStart: 0,
31
+ listingEnd: 0,
32
+ trace: null,
33
+ withRanges: false,
34
+ });
35
+ const pass1Content = [pass1Toc, ...sections, constants_1.MD_FOOTER].join("\n");
36
+ // 2) Analyze the FINAL STRING (pass 1) to get real line indexes
37
+ const pass1Analysis = analyzeTrace(pass1Content, count);
38
+ // 3) Build pass 2 TOC including listing range + per-file ranges
39
+ const pass2Toc = buildToc({
40
+ files,
41
+ count,
42
+ listingStart: pass1Analysis.listingStart,
43
+ listingEnd: pass1Analysis.listingEnd,
44
+ trace: pass1Analysis.trace,
45
+ withRanges: true,
46
+ });
47
+ const pass2Content = [pass2Toc, ...sections, constants_1.MD_FOOTER].join("\n");
48
+ // 4) Re-analyze pass 2 content (should be identical; extra safety)
49
+ const pass2Analysis = analyzeTrace(pass2Content, count);
50
+ // If anything drifted (it really shouldn’t), trust pass 2’s analysis.
51
+ // Rebuilding again would still not change line counts, so this is stable.
52
+ return {
53
+ content: pass2Content,
54
+ trace: pass2Analysis.trace,
55
+ listingStart: pass2Analysis.listingStart,
56
+ listingEnd: pass2Analysis.listingEnd,
57
+ };
58
+ }
59
+ const rangeText = (start, end) => ` L${start}-L${end}`;
60
+ function buildToc(opts) {
61
+ const { files, count, listingStart, listingEnd, trace, withRanges } = opts;
62
+ const tocHead = [
63
+ `# Index ${withRanges ? rangeText(listingStart, listingEnd) : ""} `,
64
+ "",
65
+ constants_1.MD_HEADER,
66
+ "",
67
+ `Included Source Files: ${count}`,
68
+ ];
69
+ const items = files.map((f, i) => {
70
+ const rp = (0, shared_2.rel)(f);
71
+ if (!withRanges || !trace)
72
+ return `- [${rp}](#${i + 1})`;
73
+ const t = trace[i];
74
+ return `- [${rp}](#${i + 1}) ${rangeText(t.startLine, t.endLine)}`;
75
+ });
76
+ const tocTail = ["", "---"];
77
+ return [...tocHead, ...items, ...tocTail].join("\n");
21
78
  }
22
79
  /**
23
- * Render each file section with invisible anchors
80
+ * Analyze the already-generated markdown to compute:
81
+ * - listing start/end lines
82
+ * - each section start/end line (per file index)
83
+ *
84
+ * All line numbers are 1-based.
24
85
  */
25
- function renderMd(p, i) {
26
- const rp = (0, io_2.rel)(p);
27
- const ext = path_1.default.extname(p).toLowerCase();
28
- const lang = render_constants_1.LANG_MAP[ext] || "txt";
29
- const code = (0, io_1.readFileSafe)(p).trimEnd();
30
- return [`---\n#### ${i + 1}`, "\n", "` File: " + rp + "` [↑ Back to top](#index)", "", "```" + lang, code, "```", ""].join("\n");
86
+ function analyzeTrace(content, count) {
87
+ const lines = content.split("\n");
88
+ // --- Listing range ---
89
+ const includedIdx = lines.findIndex((l) => /^Included Source Files\b/.test(l.trim()));
90
+ // listing begins on the next line after the header
91
+ const listingStart = includedIdx >= 0 ? includedIdx + 2 : 0; // 1-based
92
+ const listingEnd = count ? listingStart + count - 1 : listingStart;
93
+ // --- Footer start (exclude footer from last file range) ---
94
+ let footerMarkerIdx = lines.findIndex((l) => l.includes("<!-- PRODEx v"));
95
+ if (footerMarkerIdx < 0)
96
+ footerMarkerIdx = lines.findIndex((l) => l.includes("*Generated with [Prodex]"));
97
+ let footerStartIdx = footerMarkerIdx >= 0 ? footerMarkerIdx : lines.length; // 0-based
98
+ if (footerStartIdx > 0 && lines[footerStartIdx - 1].trim() === "---")
99
+ footerStartIdx = footerStartIdx - 1;
100
+ // --- Section markers: find "#### N" lines ---
101
+ const markerLineIdxByN = new Map();
102
+ for (let i = 0; i < lines.length; i++) {
103
+ const m = lines[i].trim().match(/^####\s+(\d+)\s*$/);
104
+ if (!m)
105
+ continue;
106
+ const n = Number(m[1]);
107
+ if (!Number.isFinite(n))
108
+ continue;
109
+ if (n < 1 || n > count)
110
+ continue;
111
+ if (!markerLineIdxByN.has(n))
112
+ markerLineIdxByN.set(n, i);
113
+ }
114
+ // Compute each section's start idx (prefer the preceding "---" line if present)
115
+ const startIdxs = [];
116
+ for (let n = 1; n <= count; n++) {
117
+ const markerIdx = markerLineIdxByN.get(n);
118
+ if (markerIdx == null) {
119
+ // Fallback: if marker missing, make it non-crashy.
120
+ // Put start at footerStart (it'll produce tiny/empty ranges rather than exploding).
121
+ startIdxs.push(footerStartIdx);
122
+ continue;
123
+ }
124
+ const maybeStart = markerIdx > 0 && lines[markerIdx - 1].trim() === "---" ? markerIdx - 1 : markerIdx;
125
+ startIdxs.push(maybeStart);
126
+ }
127
+ // Compute end idx using next section start, or footer start
128
+ const trace = [];
129
+ for (let i = 0; i < count; i++) {
130
+ const startIdx = startIdxs[i];
131
+ const nextStartIdx = i < count - 1 ? startIdxs[i + 1] : footerStartIdx;
132
+ const endIdx = Math.max(startIdx, nextStartIdx - 1);
133
+ trace.push({
134
+ file: "", // filled by caller if needed; TOC uses rel(files[i]) anyway
135
+ anchor: i + 1,
136
+ startLine: startIdx + 1,
137
+ endLine: endIdx + 1,
138
+ });
139
+ }
140
+ return { listingStart, listingEnd, trace };
31
141
  }
32
142
  /**
33
- * TXT version (unchanged)
143
+ * Existing functions kept as-is.
144
+ * (renderMd is used by renderTraceMd above)
34
145
  */
146
+ function tocMd(files) {
147
+ const count = files.length;
148
+ const items = files.map((f, i) => `- [${(0, shared_2.rel)(f)}](#${i + 1})`).join("\n");
149
+ return ["# Index ", `\nIncluded Source Files (${count})`, items, "", "---"].join("\n");
150
+ }
151
+ function renderMd(p, i) {
152
+ const rp = (0, shared_2.rel)(p);
153
+ const ext = path_1.default.extname(p).toLowerCase();
154
+ const lang = constants_1.LANG_MAP[ext] || "txt";
155
+ const code = (0, shared_1.readFileSafe)(p).trimEnd();
156
+ return [
157
+ `---\n#### ${i + 1}`,
158
+ "\n",
159
+ "` File: " + rp + "` [↑ Back to top](#index)",
160
+ "",
161
+ "```" + lang,
162
+ code,
163
+ "```",
164
+ "",
165
+ ].join("\n");
166
+ }
167
+ // TXT versions unchanged
35
168
  function tocTxt(files) {
36
169
  const sorted = [...files].sort((a, b) => a.localeCompare(b));
37
- return ["##==== Combined Scope ====", ...sorted.map((f) => "## - " + (0, io_2.rel)(f))].join("\n") + "\n\n";
170
+ return ["##==== Combined Scope ====", ...sorted.map((f) => "## - " + (0, shared_2.rel)(f))].join("\n") + "\n\n";
38
171
  }
39
172
  function renderTxt(p) {
40
- const relPath = (0, io_2.rel)(p);
41
- const code = (0, io_1.readFileSafe)(p);
173
+ const relPath = (0, shared_2.rel)(p);
174
+ const code = (0, shared_1.readFileSafe)(p);
42
175
  return ["##==== path: " + relPath + " ====", "##region " + relPath, code, "##endregion", ""].join("\n");
43
176
  }
package/dist/index.js CHANGED
@@ -44,12 +44,51 @@ async function startProdex(args = process.argv) {
44
44
  return (0, init_1.initProdex)();
45
45
  const { root, flags } = (0, cli_input_1.parseCliInput)(args);
46
46
  const userConfig = config_1.ConfigManager.load(root);
47
- const config = config_1.ConfigManager.merge(userConfig, flags, root);
48
- (0, store_1.setGlobals)(config, flags);
47
+ // Determine which shortcut runs to execute (order irrelevant).
48
+ const selected = [];
49
+ if (flags.shortcutAll) {
50
+ selected.push(...Object.keys(userConfig.shortcuts ?? {}));
51
+ }
52
+ else if (Array.isArray(flags.shortcuts) && flags.shortcuts.length) {
53
+ selected.push(...flags.shortcuts);
54
+ }
55
+ else if (flags.shortcut) {
56
+ selected.push(flags.shortcut);
57
+ }
58
+ const shortcutRuns = Array.from(new Set(selected.map((s) => String(s).trim()).filter(Boolean))).sort();
59
+ await Promise.resolve().then(() => __importStar(require("./lib/polyfills")));
60
+ // Multi-run mode (shortcuts)
61
+ if (shortcutRuns.length) {
62
+ for (const shortcut of shortcutRuns) {
63
+ const runFlags = {
64
+ ...flags,
65
+ shortcut,
66
+ shortcuts: undefined,
67
+ shortcutAll: false,
68
+ };
69
+ const config = config_1.ConfigManager.merge(userConfig, runFlags, root);
70
+ (0, store_1.setGlobals)(config, runFlags);
71
+ const opts = {
72
+ showUi: false,
73
+ // Avoid output collisions when running multiple shortcuts.
74
+ cliName: config.name ?? shortcut,
75
+ };
76
+ await (0, combine_1.runCombine)({ cfg: config, opts });
77
+ }
78
+ return;
79
+ }
80
+ // If "@" was used but no shortcuts exist, fall back to a normal single run.
81
+ const baseFlags = flags.shortcutAll ? { ...flags, shortcutAll: false } : flags;
82
+ const config = config_1.ConfigManager.merge(userConfig, baseFlags, root);
83
+ (0, store_1.setGlobals)(config, baseFlags);
49
84
  const opts = {
50
- showUi: !flags.ci && !flags?.files?.length && config?.entry?.ui?.enablePicker && !flags.shortcut,
85
+ showUi: !baseFlags.ci &&
86
+ !baseFlags?.files?.length &&
87
+ config?.entry?.ui?.enablePicker &&
88
+ !baseFlags.shortcut &&
89
+ !baseFlags.shortcuts?.length &&
90
+ !baseFlags.shortcutAll,
51
91
  cliName: config.name,
52
92
  };
53
- await Promise.resolve().then(() => __importStar(require("./lib/polyfills")));
54
93
  await (0, combine_1.runCombine)({ cfg: config, opts });
55
94
  }
@@ -8,9 +8,14 @@ const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const cache_1 = require("../../core/managers/cache");
10
10
  const cache_keys_1 = require("../../constants/cache-keys");
11
+ const extract_imports_1 = require("./extract-imports");
12
+ const logger_1 = require("../../lib/logger");
11
13
  /**
12
- * Scans app/Providers/*.php for `$this->app->bind()` / `singleton()` calls
13
- * and returns a map of Interface::classImplementation::class (FQCN strings).
14
+ * Scans app/Providers/*.php for $this->app->bind() / singleton() calls
15
+ * and returns a map of InterfaceFQCNImplementationFQCN.
16
+ *
17
+ * Uses existing extractPhpImports + expandGroupedUses to correctly
18
+ * resolve namespaces and short class names.
14
19
  */
15
20
  function loadLaravelBindings(root) {
16
21
  const cached = cache_1.CacheManager.get(cache_keys_1.CACHE_KEYS.PHP_BINDINGS, root);
@@ -26,16 +31,27 @@ function loadLaravelBindings(root) {
26
31
  .readdirSync(providersDir)
27
32
  .filter((f) => f.endsWith(".php"))
28
33
  .map((f) => path_1.default.join(providersDir, f));
29
- // $this->app->bind(Interface::class, Implementation::class)
30
- // $this->app->singleton(Interface::class, Implementation::class)
31
- const re = /\$this->app->(?:bind|singleton)\s*\(\s*([A-Za-z0-9_:\\\\]+)::class\s*,\s*([A-Za-z0-9_:\\\\]+)::class/g;
32
34
  for (const file of files) {
33
35
  const code = fs_1.default.readFileSync(file, "utf8");
36
+ // 1️⃣ Extract all imports in the provider
37
+ const rawImports = (0, extract_imports_1.extractPhpImports)(code);
38
+ const expanded = (0, extract_imports_1.expandGroupedUses)(rawImports);
39
+ // Build ShortName → FQCN map
40
+ const importMap = {};
41
+ for (const fqcn of expanded) {
42
+ const short = fqcn.split("\\").pop();
43
+ importMap[short] = fqcn;
44
+ }
45
+ // 2️⃣ Extract bindings (short class names only)
46
+ const bindRe = /\$this->app->(?:bind|singleton)\s*\(\s*([A-Za-z0-9_]+)::class\s*,\s*([A-Za-z0-9_]+)::class/g;
34
47
  let m;
35
- while ((m = re.exec(code))) {
36
- const iface = m[1].replace(/\\\\/g, "\\");
37
- const impl = m[2].replace(/\\\\/g, "\\");
38
- bindings[iface] = impl;
48
+ while ((m = bindRe.exec(code))) {
49
+ const ifaceShort = m[1];
50
+ const implShort = m[2];
51
+ const ifaceFull = importMap[ifaceShort] || ifaceShort;
52
+ const implFull = importMap[implShort] || implShort;
53
+ logger_1.logger.debug(`[laravel-bindings] ${file} => ${ifaceFull} → ${implFull}`);
54
+ bindings[ifaceFull] = implFull;
39
55
  }
40
56
  }
41
57
  cache_1.CacheManager.set(cache_keys_1.CACHE_KEYS.PHP_BINDINGS, root, bindings);
@@ -10,10 +10,7 @@ exports.expandGroupedUses = expandGroupedUses;
10
10
  */
11
11
  function extractPhpImports(code) {
12
12
  const out = new Set();
13
- const patterns = [
14
- /\b(?:require|include|require_once|include_once)\s*\(?['"]([^'"]+)['"]\)?/g,
15
- /\buse\s+([A-Z][\w\\]+(?:\s*{[^}]+})?)/g,
16
- ];
13
+ const patterns = [/\b(?:require|include|require_once|include_once)\s*\(?['"]([^'"]+)['"]\)?/g, /\buse\s+([A-Z][\w\\]+(?:\s*{[^}]+})?)/g];
17
14
  for (const r of patterns) {
18
15
  let m;
19
16
  while ((m = r.exec(code))) {
@@ -22,6 +19,23 @@ function extractPhpImports(code) {
22
19
  out.add(val);
23
20
  }
24
21
  }
22
+ // Detect short class names
23
+ const shortClassPatterns = [/\bnew\s+([A-Z][A-Za-z0-9_]*)\b/g, /\b([A-Z][A-Za-z0-9_]*)::class\b/g, /\b([A-Z][A-Za-z0-9_]*)::[A-Za-z_]/g, /:\s*([A-Z][A-Za-z0-9_]*)\b/g];
24
+ // Direct matches
25
+ for (const r of shortClassPatterns) {
26
+ let m;
27
+ while ((m = r.exec(code)))
28
+ out.add(m[1]);
29
+ }
30
+ // Type-hinted parameters in function signatures
31
+ let m;
32
+ const paramPattern = /\(([^)]*)\)/g;
33
+ while ((m = paramPattern.exec(code))) {
34
+ const block = m[1];
35
+ const types = block.match(/\b([A-Z][A-Za-z0-9_]*)\b/g);
36
+ if (types)
37
+ types.forEach((t) => out.add(t));
38
+ }
25
39
  return out;
26
40
  }
27
41
  /**
@@ -16,22 +16,6 @@ const constants_1 = require("../../constants");
16
16
  const cache_1 = require("../../core/managers/cache");
17
17
  const collections_1 = require("../../shared/collections");
18
18
  const promises_1 = __importDefault(require("fs/promises")); // (add near the top if not present)
19
- /** Ensure we have a PHP resolver context for the current root */
20
- function buildPhpCtx(root, prev) {
21
- if (prev?.kind === "php")
22
- return prev;
23
- const psr4 = (0, psr4_1.resolvePsr4)(root);
24
- const nsKeys = Object.keys(psr4).sort((a, b) => b.length - a.length);
25
- const bindings = (0, bindings_1.loadLaravelBindings)(root);
26
- return { kind: "php", psr4, nsKeys, bindings };
27
- }
28
- /** Namespace prefix check */
29
- function startsWithAnyNamespace(imp, nsKeys) {
30
- for (const k of nsKeys)
31
- if (imp.startsWith(k))
32
- return true;
33
- return false;
34
- }
35
19
  /**
36
20
  * Typed PHP resolver (aligned with JS resolver signature).
37
21
  * - Uses global config via getConfig()
@@ -52,6 +36,8 @@ async function resolvePhpImports({ filePath, visited = new Set(), depth = 0, max
52
36
  const code = (0, shared_1.readFileSafe)(filePath);
53
37
  if (!code)
54
38
  return (0, collections_1.emptyResult)(visited);
39
+ const nsMatch = code.match(/\bnamespace\s+([^;]+);/);
40
+ const currentNamespace = nsMatch ? nsMatch[1].trim() : null;
55
41
  // Context + exclusions
56
42
  const phpCtx = buildPhpCtx(ROOT, ctx);
57
43
  // Parse imports (expand grouped `use` syntax)
@@ -62,15 +48,21 @@ async function resolvePhpImports({ filePath, visited = new Set(), depth = 0, max
62
48
  const filesOut = [];
63
49
  for (const imp0 of imports) {
64
50
  let imp = imp0;
51
+ if (!imp || typeof imp !== "string")
52
+ continue;
53
+ // Fully-qualified check
54
+ const isFullyQualified = imp.includes("\\") || imp.startsWith("\\");
55
+ if (!isFullyQualified && currentNamespace) {
56
+ imp = `${currentNamespace}\\${imp}`;
57
+ }
65
58
  // Respect Laravel container bindings (Interface → Implementation)
66
59
  if (phpCtx.bindings[imp]) {
67
- logger_1.logger.debug("[php-resolver] binding:", imp, "→", phpCtx.bindings[imp]);
60
+ // logger.debug("[php-resolver] binding:", imp, "→", _2j(phpCtx.bindings[imp]));
68
61
  imp = phpCtx.bindings[imp];
69
62
  }
70
63
  // Only resolve PSR-4 mapped namespaces
71
64
  if (!startsWithAnyNamespace(imp, phpCtx.nsKeys))
72
65
  continue;
73
- // if (isExcluded(imp, excludePatterns, ROOT)) continue;
74
66
  // Resolve namespace → file path (sync helper retained)
75
67
  const resolvedPath = await tryResolvePhpFile(imp, filePath, phpCtx.psr4);
76
68
  // Exclusion check after final resolution
@@ -109,7 +101,7 @@ async function tryResolvePhpFile(imp, fromFile, psr4) {
109
101
  cache_1.CacheManager.set(constants_1.CACHE_KEYS.PHP_FILECACHE, key, null);
110
102
  return null;
111
103
  }
112
- const rel = imp.slice(nsKey.length).norm();
104
+ const rel = imp.replace(nsKey, "").norm();
113
105
  const tries = [path_1.default.join(psr4[nsKey], rel), path_1.default.join(psr4[nsKey], rel + ".php"), path_1.default.join(psr4[nsKey], rel, "index.php")];
114
106
  // 🔹 Run all stats concurrently
115
107
  const results = await Promise.allSettled(tries.map(async (p) => {
@@ -126,3 +118,19 @@ async function tryResolvePhpFile(imp, fromFile, psr4) {
126
118
  cache_1.CacheManager.set(constants_1.CACHE_KEYS.PHP_FILECACHE, key, resolved);
127
119
  return resolved;
128
120
  }
121
+ /** Ensure we have a PHP resolver context for the current root */
122
+ function buildPhpCtx(root, prev) {
123
+ if (prev?.kind === "php")
124
+ return prev;
125
+ const psr4 = (0, psr4_1.resolvePsr4)(root);
126
+ const nsKeys = Object.keys(psr4).sort((a, b) => b.length - a.length);
127
+ const bindings = (0, bindings_1.loadLaravelBindings)(root);
128
+ return { kind: "php", psr4, nsKeys, bindings };
129
+ }
130
+ /** Namespace prefix check */
131
+ function startsWithAnyNamespace(imp, nsKeys) {
132
+ for (const k of nsKeys)
133
+ if (imp.startsWith(k))
134
+ return true;
135
+ return false;
136
+ }
@@ -4,21 +4,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.isExcluded = isExcluded;
7
- exports.makeExcludeMatcher = makeExcludeMatcher;
8
7
  // File: src/shared/patterns.ts
9
8
  const micromatch_1 = __importDefault(require("micromatch"));
10
9
  const path_1 = __importDefault(require("path"));
11
10
  const _1 = require(".");
12
- /**
13
- * Returns true if a given path matches any of the provided glob patterns.
14
- * Equivalent to core/helpers.isExcluded().
15
- */
16
- // export function isExcluded(p: string = "", patterns: string[] = [], root = process.cwd()): boolean {
17
- // if (!patterns?.length) return false;
18
- // if (!p) return false;
19
- // const relPath = p.replaceAll("\\", "/");
20
- // return micromatch.isMatch(relPath, patterns);
21
- // }
22
11
  /**
23
12
  * Centralized exclusion logic.
24
13
  * Accepts namespaces, absolute paths, or relative paths
@@ -36,13 +25,3 @@ function isExcluded(p, patterns = [], root = process.cwd()) {
36
25
  norm = (0, _1.rel)(norm, root).norm();
37
26
  return micromatch_1.default.isMatch(norm, patterns);
38
27
  }
39
- /**
40
- * Builds a reusable micromatch matcher for efficiency.
41
- * Equivalent to php-resolver.makeExcludeMatcher().
42
- */
43
- function makeExcludeMatcher(patterns = []) {
44
- if (!patterns?.length)
45
- return () => false;
46
- const mm = micromatch_1.default.matcher(patterns);
47
- return (s) => mm(String(s).replace(/\\/g, "/"));
48
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prodex",
3
- "version": "1.4.6",
3
+ "version": "1.4.8",
4
4
  "description": "Unified Project Indexer & Dependency Extractor for Laravel + React + Node stacks.",
5
5
  "type": "commonjs",
6
6
  "bin": {