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.
- package/dist/cli/cli-input.js +49 -15
- package/dist/constants/render-constants.js +2 -1
- package/dist/core/combine.js +2 -2
- package/dist/core/dependency.js +26 -3
- package/dist/core/managers/cache.js +1 -1
- package/dist/core/managers/config.js +4 -1
- package/dist/core/output.js +4 -3
- package/dist/core/renderers.js +151 -18
- package/dist/index.js +43 -4
- package/dist/resolvers/php/bindings.js +25 -9
- package/dist/resolvers/php/extract-imports.js +18 -4
- package/dist/resolvers/php/php-resolver.js +27 -19
- package/dist/shared/patterns.js +0 -21
- package/package.json +1 -1
package/dist/cli/cli-input.js
CHANGED
|
@@ -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:
|
|
33
|
-
root:
|
|
50
|
+
rootArg: root,
|
|
51
|
+
root: root ? path_1.default.resolve(cwd, root) : cwd,
|
|
34
52
|
flags: { ...opts },
|
|
35
53
|
};
|
|
36
54
|
});
|
|
37
|
-
program.parse(
|
|
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
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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)*";
|
package/dist/core/combine.js
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
}
|
package/dist/core/dependency.js
CHANGED
|
@@ -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
|
|
73
|
-
|
|
74
|
-
|
|
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} → ${
|
|
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
|
-
|
|
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);
|
package/dist/core/output.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/core/renderers.js
CHANGED
|
@@ -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
|
|
12
|
-
const
|
|
13
|
-
const
|
|
12
|
+
const shared_1 = require("../shared");
|
|
13
|
+
const shared_2 = require("../shared");
|
|
14
|
+
const constants_1 = require("../constants");
|
|
14
15
|
/**
|
|
15
|
-
*
|
|
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
|
-
|
|
21
|
+
// src/core/renderers.ts
|
|
22
|
+
function renderTraceMd(files) {
|
|
18
23
|
const count = files.length;
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
*
|
|
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
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
*
|
|
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,
|
|
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,
|
|
41
|
-
const code = (0,
|
|
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
|
-
|
|
48
|
-
|
|
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: !
|
|
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
|
|
13
|
-
* and returns a map of
|
|
14
|
+
* Scans app/Providers/*.php for $this->app->bind() / singleton() calls
|
|
15
|
+
* and returns a map of InterfaceFQCN → ImplementationFQCN.
|
|
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 =
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|
package/dist/shared/patterns.js
CHANGED
|
@@ -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
|
-
}
|