prodex 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Zeki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,197 @@
1
+ # 🧩 Prodex — Unified Project Indexer & Dependency Extractor
2
+
3
+ **Prodex** *(short for “Project Index”)* is a smart cross-language dependency combiner for modern web stacks — built to traverse **Laravel + React + TypeScript** projects and extract a clean, flattened scope of every linked file.
4
+
5
+ Whether you’re debugging imports, building AI context files, or simply auditing what your app actually depends on — Prodex builds you a unified **project index** in seconds.
6
+
7
+ ---
8
+
9
+ ## 🚀 Key Features
10
+
11
+ | Feature | Description |
12
+ |----------|-------------|
13
+ | ⚙️ **Cross-language resolver** | Understands both JavaScript / TypeScript (`import`, `require`, `export * from`) and PHP (`use`, `require`, `include`) dependency trees. |
14
+ | 🧩 **Laravel-aware bindings** | Reads your `app/Providers` and automatically maps interfaces to their concrete implementations. |
15
+ | 🧭 **Smart alias detection** | Parses `tsconfig.json` and `vite.config.*` for alias paths (`@/components/...`). |
16
+ | 🗂 **Grouped imports support** | Expands `use App\Http\Controllers\{A,B,C}` into individual files. |
17
+ | 🔄 **Recursive chain following** | Walks through imports, re-exports, and PSR-4 namespaces up to your configured depth. |
18
+ | 🪶 **Clean combined output** | Merges every resolved file into one `.txt` or `.md` file with region markers for readability. |
19
+ | 🧠 **Static & safe** | No runtime PHP execution — everything is parsed statically via regex + PSR-4 mapping. |
20
+
21
+ ---
22
+
23
+ ## 📦 Installation
24
+
25
+ ```bash
26
+ npm install -g prodex
27
+ ```
28
+
29
+ or locally:
30
+
31
+ ```bash
32
+ npm install --save-dev prodex
33
+ ```
34
+
35
+ ---
36
+
37
+ ## 🧰 Usage
38
+
39
+ Run directly from your project root:
40
+
41
+ ```bash
42
+ prodex
43
+ ```
44
+
45
+ You’ll be guided through an interactive CLI:
46
+
47
+ ```
48
+ 🧩 Prodex — Project Dependency Extractor
49
+ 🧩 Active Config:
50
+ • Output: ./combined.txt
51
+ • Scan Depth: 2
52
+ • Base Dirs: app, routes, resources/js
53
+ ```
54
+
55
+ After selecting files and confirming, Prodex generates:
56
+
57
+ ```
58
+ ✅ combined.txt written (12 file(s)).
59
+ ```
60
+
61
+ Each file appears wrapped in annotated regions:
62
+
63
+ ```
64
+ // ==== path: app/Services/Shots/ComputeService.php ====
65
+ // #region app/Services/Shots/ComputeService.php
66
+ <?php
67
+ // your code here...
68
+ // #endregion
69
+ ```
70
+
71
+ ---
72
+
73
+ ## ⚙️ Configuration
74
+
75
+ Create a `.prodex.json` in your project root (optional):
76
+
77
+ ```jsonc
78
+ {
79
+ "output": "./combined.txt",
80
+ "scanDepth": 3,
81
+ "baseDirs": ["app", "routes", "resources/js"],
82
+ "entryExcludes": ["vendor", "node_modules"],
83
+ "importExcludes": ["vendor", "tests"]
84
+ }
85
+ ```
86
+
87
+ Prodex automatically merges this with sane defaults.
88
+
89
+ ---
90
+
91
+ ## 🧩 How It Works
92
+
93
+ **1. Config Loader**
94
+ - Reads `.prodex.json`, `tsconfig.json`, and `vite.config.*`.
95
+ - Builds alias + exclusion map.
96
+
97
+ **2. JS Resolver**
98
+ - Extracts ES modules, dynamic imports, and re-exports.
99
+ - Resolves alias paths to absolute file locations.
100
+
101
+ **3. PHP Resolver**
102
+ - Parses `use`, grouped `use {}`, `require`, and `include`.
103
+ - Expands PSR-4 namespaces via `composer.json`.
104
+ - Loads bindings from all `app/Providers/*.php` to link interfaces to implementations.
105
+
106
+ **4. Combiner**
107
+ - Follows all dependency chains (recursive up to limit).
108
+ - Writes a single combined file with a TOC and inline region markers.
109
+
110
+ ---
111
+
112
+ ## 🧱 Example: Laravel + React Project
113
+
114
+ ```bash
115
+ prodex
116
+ ```
117
+
118
+ ```
119
+ 🧩 Following dependency chain...
120
+ ✅ combined.txt written (24 file(s)).
121
+ ```
122
+
123
+ Included files:
124
+
125
+ ```
126
+ resources/js/pages/accounts.tsx
127
+ app/Http/Controllers/Shots/AccountsController.php
128
+ app/Repositories/Shots/FireflyApiRepository.php
129
+ app/Enums/Shots/Granularity.php
130
+ app/Support/Shots/CacheKeys.php
131
+ ...
132
+ ```
133
+
134
+ ---
135
+
136
+ ## 🧠 Ideal Use Cases
137
+
138
+ - 🧩 Feeding combined source to **AI assistants / context engines**
139
+ - 🧪 Static dependency audits or architecture mapping
140
+ - 🧰 Quick “code snapshot” before refactors
141
+ - 📄 Documentation generation / single-file review
142
+
143
+ ---
144
+
145
+ ## 🔧 CLI Flags (optional)
146
+
147
+ | Flag | Description |
148
+ |------|-------------|
149
+ | `--depth <n>` | Override scan depth |
150
+ | `--output <path>` | Custom output path |
151
+ | `--no-chain` | Disable dependency recursion |
152
+ | `--debug` | Enable verbose logging |
153
+
154
+ Example:
155
+
156
+ ```bash
157
+ prodex --depth 3 --output ./dump.txt --debug
158
+ ```
159
+
160
+ ---
161
+
162
+ ## 🧾 Publishing to npm
163
+
164
+ ```bash
165
+ # Build and test locally
166
+ npm run build
167
+
168
+ # Version bump
169
+ npm version patch # or minor / major
170
+
171
+ # Log in and publish
172
+ npm login
173
+ npm publish --access public
174
+ ```
175
+
176
+ ---
177
+
178
+ ## 💡 Philosophy
179
+
180
+ Prodex isn’t a linter or bundler —
181
+ it’s an **indexer** that unifies multi-language project contexts for smarter automation, analysis, and AI-assisted workflows.
182
+
183
+ Built with care for mixed stacks like **Laravel + Inertia + React**,
184
+ and designed to be both *safe* and *predictable.*
185
+
186
+ ---
187
+
188
+ ## 🧑‍💻 Maintained by **emxhive**
189
+
190
+ Prodex is part of the **Forge** developer ecosystem by **emxhive**,
191
+ a suite of modular tools for Laravel + React applications.
192
+
193
+ ---
194
+
195
+ ## 🧾 License
196
+
197
+ MIT © 2025 emxhive
package/bin/prodex.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import("../src/index.js").then(({ default: startProdex }) => startProdex());
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "prodex",
3
+ "version": "1.0.0",
4
+ "description": "Unified Project Indexer & Dependency Extractor for Laravel + React stacks.",
5
+ "type": "module",
6
+ "bin": {
7
+ "prodex": "./bin/prodex.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "keywords": ["laravel", "react", "typescript", "dependency", "analyzer", "cli"],
16
+ "author": "emxhive",
17
+ "license": "MIT"
18
+ }
@@ -0,0 +1,49 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import inquirer from "inquirer";
4
+
5
+ export async function initProdex() {
6
+ console.log("🪄 Prodex Init — Configuration Wizard\n");
7
+
8
+ const dest = path.join(process.cwd(), ".prodex.json");
9
+ if (fs.existsSync(dest)) {
10
+ const { overwrite } = await inquirer.prompt([
11
+ { type: "confirm", name: "overwrite", message: ".prodex.json already exists. Overwrite?", default: false }
12
+ ]);
13
+ if (!overwrite) {
14
+ console.log("❌ Cancelled.");
15
+ return;
16
+ }
17
+ }
18
+
19
+ const jsonc = `{
20
+ // -------------------------------------------------------------
21
+ // 🧩 Prodex Configuration
22
+ // -------------------------------------------------------------
23
+ // Customize how Prodex flattens your project.
24
+ // For docs, visit: https://github.com/emxhive/prodex#configuration
25
+ // -------------------------------------------------------------
26
+
27
+ "$schema": "https://raw.githubusercontent.com/emxhive/prodex/main/schema/prodex.schema.json",
28
+
29
+ "output": "combined.txt",
30
+ "scanDepth": 2,
31
+ "baseDirs": ["app", "routes", "resources/js"],
32
+ "aliasOverrides": {
33
+ "@hooks": "resources/js/hooks",
34
+ "@data": "resources/js/data"
35
+ },
36
+ "entryExcludes": [
37
+ "resources/js/components/ui/",
38
+ "app/DTOs/"
39
+ ],
40
+ "importExcludes": [
41
+ "node_modules",
42
+ "@shadcn/"
43
+ ]
44
+ }`;
45
+
46
+ fs.writeFileSync(dest, jsonc, "utf8");
47
+ console.log(`✅ Created ${dest}`);
48
+ console.log("💡 You can edit it anytime or rerun 'prodex init' to reset.");
49
+ }
@@ -0,0 +1,8 @@
1
+ import inquirer from "inquirer";
2
+
3
+ export async function pickEntries() {
4
+ const { path } = await inquirer.prompt([
5
+ { name: "path", message: "Enter path to combine:", default: "src" }
6
+ ]);
7
+ return [path];
8
+ }
@@ -0,0 +1,4 @@
1
+ export function printSummary(entries) {
2
+ console.log("\nYou selected:");
3
+ for (const e of entries) console.log(" -", e);
4
+ }
@@ -0,0 +1,47 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import {
4
+ ROOT,
5
+ OUT_FILE,
6
+ CODE_EXTS,
7
+ ENTRY_EXCLUDES,
8
+ IMPORT_EXCLUDES,
9
+ BASE_DIRS
10
+ } from "./config.js";
11
+
12
+ export function loadProdexConfig() {
13
+ const configPath = path.join(ROOT, ".prodex.json");
14
+ let userConfig = {};
15
+
16
+ if (fs.existsSync(configPath)) {
17
+ try {
18
+ const data = fs.readFileSync(configPath, "utf8");
19
+ userConfig = JSON.parse(data);
20
+ console.log("🧠 Loaded .prodex.json overrides");
21
+ } catch (err) {
22
+ console.warn("⚠️ Failed to parse .prodex.json:", err.message);
23
+ }
24
+ }
25
+
26
+ const merged = {
27
+ output: userConfig.output || OUT_FILE,
28
+ scanDepth: userConfig.scanDepth || 2,
29
+ codeExts: userConfig.codeExts || CODE_EXTS,
30
+ entryExcludes: [...ENTRY_EXCLUDES, ...(userConfig.entryExcludes || [])],
31
+ importExcludes: [...IMPORT_EXCLUDES, ...(userConfig.importExcludes || [])],
32
+ baseDirs: [...new Set([...(userConfig.baseDirs || []), ...BASE_DIRS])],
33
+ aliasOverrides: userConfig.aliasOverrides || {}
34
+ };
35
+
36
+ console.log("🧩 Active Config:");
37
+ console.log(" • Output:", merged.output);
38
+ console.log(" • Scan Depth:", merged.scanDepth);
39
+ console.log(" • Base Dirs:", merged.baseDirs.join(", "));
40
+ if (userConfig.entryExcludes || userConfig.importExcludes)
41
+ console.log(" • Custom Exclusions:", {
42
+ entries: userConfig.entryExcludes?.length || 0,
43
+ imports: userConfig.importExcludes?.length || 0
44
+ });
45
+
46
+ return merged;
47
+ }
@@ -0,0 +1,26 @@
1
+ export const ROOT = process.cwd();
2
+ export const OUT_FILE = ROOT + "/combined.txt";
3
+ export const CODE_EXTS = [".ts", ".tsx", ".d.ts", ".php"];
4
+ export const ENTRY_EXCLUDES = [
5
+ "resources/js/components/ui/",
6
+ "app/Enums/",
7
+ "app/DTOs/",
8
+ "app/Models/",
9
+ "app/Data/",
10
+ "resources/js/wayfinder/",
11
+ "resources/js/routes/",
12
+ "resources/js/actions/",
13
+ "resources/js/hooks/"
14
+ ];
15
+ export const IMPORT_EXCLUDES = [
16
+ "node_modules",
17
+ "@shadcn/",
18
+ "@/components/ui/",
19
+ "@components/ui/",
20
+ "resources/js/components/ui/",
21
+ "resources/js/hooks/",
22
+ "resources/js/wayfinder/",
23
+ "resources/js/routes/",
24
+ "resources/js/actions/"
25
+ ];
26
+ export const BASE_DIRS = ["app", "routes", "resources/js"];
@@ -0,0 +1,8 @@
1
+ export function loadJsAliases() {
2
+ // will later handle vite + tsconfig
3
+ return {};
4
+ }
5
+ export function loadComposerAliases() {
6
+ // will later handle composer.json PSR-4
7
+ return {};
8
+ }
@@ -0,0 +1,129 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import inquirer from "inquirer";
4
+ import { ROOT } from "../constants/config.js";
5
+ import { loadProdexConfig } from "../constants/config-loader.js";
6
+ import { read, normalizeIndent, stripComments, walk, rel } from "./helpers.js";
7
+ import { resolveJsImports } from "../resolvers/js-resolver.js";
8
+ import { resolvePhpImports } from "../resolvers/php-resolver.js";
9
+
10
+ export async function runCombine() {
11
+ const cfg = loadProdexConfig();
12
+ const { output, baseDirs, scanDepth } = cfg;
13
+
14
+ const entries = await pickEntries(baseDirs, scanDepth);
15
+ if (!entries.length) {
16
+ console.log("❌ No entries selected.");
17
+ return;
18
+ }
19
+
20
+ const { chain, limit, proceed } = await pickSettings(entries);
21
+ if (!proceed) {
22
+ console.log("⚙️ Aborted.");
23
+ return;
24
+ }
25
+
26
+ const finalFiles = chain ? await followChain(entries, limit) : entries;
27
+ fs.writeFileSync(output, [toc(finalFiles), ...finalFiles.map(render)].join(""), "utf8");
28
+ console.log(`\n✅ ${output} written (${finalFiles.length} file(s)).`);
29
+ }
30
+
31
+ // ---------- UI ----------
32
+ async function pickEntries(baseDirs, depth = 2) {
33
+ let selected = [];
34
+ while (true) {
35
+ const files = [];
36
+ for (const base of baseDirs) {
37
+ const full = path.join(ROOT, base);
38
+ if (!fs.existsSync(full)) continue;
39
+ for (const f of walk(full, 0, depth)) files.push(f);
40
+ }
41
+
42
+ const choices = files.map(f => ({ name: rel(f), value: f }));
43
+ choices.push(new inquirer.Separator());
44
+ choices.push({ name: "🔽 Load more (go deeper)", value: "__loadmore" });
45
+ choices.push({ name: "📝 Enter custom path", value: "__manual" });
46
+
47
+ const { picks } = await inquirer.prompt([
48
+ {
49
+ type: "checkbox",
50
+ name: "picks",
51
+ message: `Select entry files (depth ${depth})`,
52
+ choices,
53
+ loop: false,
54
+ pageSize: 20,
55
+ default: selected
56
+ }
57
+ ]);
58
+
59
+ if (picks.includes("__manual")) {
60
+ const { manual } = await inquirer.prompt([{ name: "manual", message: "Enter relative path:" }]);
61
+ if (manual.trim()) selected.push(path.resolve(ROOT, manual.trim()));
62
+ }
63
+
64
+ if (picks.includes("__loadmore")) {
65
+ depth++;
66
+ selected = picks.filter(p => !["__manual", "__loadmore"].includes(p));
67
+ continue;
68
+ }
69
+
70
+ selected = picks.filter(p => !["__manual", "__loadmore"].includes(p));
71
+ break;
72
+ }
73
+ return [...new Set(selected)];
74
+ }
75
+
76
+ async function pickSettings(entries) {
77
+ console.log("\n📋 You selected:");
78
+ for (const e of entries) console.log(" -", rel(e));
79
+ const ans = await inquirer.prompt([
80
+ { type: "confirm", name: "chain", message: "Follow dependency chain?", default: true },
81
+ { type: "number", name: "limit", message: "Limit number of merged files:", default: 200, validate: v => (!isNaN(v) && v > 0) || "Enter valid number" },
82
+ { type: "confirm", name: "proceed", message: "Proceed with combine?", default: true }
83
+ ]);
84
+ return ans;
85
+ }
86
+
87
+ // ---------- Combine logic ----------
88
+ function header(p) { return `// ==== path: ${rel(p)} ====`; }
89
+ function regionStart(p) { return `// #region ${rel(p)}`; }
90
+ const regionEnd = "// #endregion";
91
+
92
+ function render(p) {
93
+ const ext = path.extname(p);
94
+ let s = read(p);
95
+ s = stripComments(s, ext);
96
+ s = normalizeIndent(s);
97
+ return `${header(p)}\n${regionStart(p)}\n${s}\n${regionEnd}\n\n`;
98
+ }
99
+
100
+ function toc(files) {
101
+ return ["// ==== Combined Scope ====", ...files.map(f => "// - " + rel(f))].join("\n") + "\n\n";
102
+ }
103
+
104
+ async function followChain(entryFiles, limit = 200) {
105
+ console.log("🧩 Following dependency chain...");
106
+ const visited = new Set();
107
+ const all = [];
108
+
109
+ for (const f of entryFiles) {
110
+ if (visited.has(f)) continue;
111
+ all.push(f);
112
+ const ext = path.extname(f);
113
+
114
+ if ([".ts", ".tsx", ".d.ts"].includes(ext)) {
115
+ const { files } = await resolveJsImports(f, visited);
116
+ all.push(...files);
117
+ } else if (ext === ".php") {
118
+ const { files } = await resolvePhpImports(f, visited);
119
+ all.push(...files);
120
+ }
121
+
122
+ if (all.length >= limit) {
123
+ console.log("⚠️ Limit reached:", limit);
124
+ break;
125
+ }
126
+ }
127
+
128
+ return [...new Set(all)];
129
+ }
@@ -0,0 +1,23 @@
1
+ export const ENTRY_EXCLUDES = [
2
+ "resources/js/components/ui/",
3
+ "app/Enums/",
4
+ "app/DTOs/",
5
+ "app/Models/",
6
+ "app/Data/",
7
+ "resources/js/wayfinder/",
8
+ "resources/js/routes/",
9
+ "resources/js/actions/",
10
+ "resources/js/hooks/",
11
+ ];
12
+
13
+ export const IMPORT_EXCLUDES = [
14
+ "node_modules",
15
+ "@shadcn/",
16
+ "@/components/ui/",
17
+ "@components/ui/",
18
+ "resources/js/components/ui/",
19
+ "resources/js/hooks/",
20
+ "resources/js/wayfinder/",
21
+ "resources/js/routes/",
22
+ "resources/js/actions/",
23
+ ];
@@ -0,0 +1,19 @@
1
+ import inquirer from "inquirer";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ export async function runCombine() {
6
+ console.log("📦 Combine mode active. (Stub)");
7
+ console.log("This is where the full combine logic from your previous script will go.\n");
8
+
9
+ const { confirm } = await inquirer.prompt([
10
+ { type: "confirm", name: "confirm", message: "Would you like to list project files?", default: true }
11
+ ]);
12
+
13
+ if (confirm) {
14
+ const files = fs.readdirSync(process.cwd());
15
+ console.log("Found files:", files);
16
+ } else {
17
+ console.log("Aborted.");
18
+ }
19
+ }
@@ -0,0 +1,51 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { ROOT, CODE_EXTS, ENTRY_EXCLUDES } from "../constants/config.js";
4
+
5
+ export function rel(p) {
6
+ return path.relative(ROOT, p).replaceAll("\\", "/");
7
+ }
8
+
9
+ export function read(p) {
10
+ try {
11
+ return fs.readFileSync(p, "utf8");
12
+ } catch {
13
+ return "";
14
+ }
15
+ }
16
+
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
+
25
+ export function stripComments(code, ext) {
26
+ let s = code.replace(/\/\*[\s\S]*?\*\//g, "");
27
+ s = s.replace(/(^|[^:])\/\/.*$/gm, (_m, p1) => p1);
28
+ if (ext === ".php") s = s.replace(/^\s*#.*$/gm, "");
29
+ return s;
30
+ }
31
+
32
+ export function isEntryExcluded(p) {
33
+ const r = rel(p);
34
+ return ENTRY_EXCLUDES.some(ex => r.startsWith(ex) || r.includes(ex));
35
+ }
36
+
37
+ export function* walk(dir, depth = 0, maxDepth = 2) {
38
+ if (depth > maxDepth) return;
39
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
40
+ for (const e of entries) {
41
+ const full = path.join(dir, e.name);
42
+ if (e.isDirectory()) yield* walk(full, depth + 1, maxDepth);
43
+ else if (e.isFile()) {
44
+ const ext = path.extname(e.name).toLowerCase();
45
+ const relPath = rel(full);
46
+ if (CODE_EXTS.includes(ext) && !ENTRY_EXCLUDES.some(ex => relPath.startsWith(ex))) {
47
+ yield full;
48
+ }
49
+ }
50
+ }
51
+ }
package/src/index.js ADDED
@@ -0,0 +1,11 @@
1
+ import { runCombine } from "./core/combine.js";
2
+ import { initProdex } from "./cli/init.js";
3
+
4
+ export default async function startProdex() {
5
+ const args = process.argv.slice(2);
6
+ if (args.includes("init")) return await initProdex();
7
+
8
+ console.clear();
9
+ console.log("🧩 Prodex — Project Dependency Extractor\n");
10
+ await runCombine();
11
+ }
@@ -0,0 +1,135 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { IMPORT_EXCLUDES, ROOT } from "../constants/config.js";
4
+
5
+ const debug = process.env.PRODEX_DEBUG === "1";
6
+ const log = (...args) => { if (debug) console.log("🪶 [resolver]", ...args); };
7
+
8
+ // --- Loaders --------------------------------------------------
9
+
10
+ function loadViteAliases() {
11
+ const files = [
12
+ "vite.config.ts",
13
+ "vite.config.js",
14
+ "vite.config.mts",
15
+ "vite.config.mjs",
16
+ "vite.config.cjs",
17
+ ];
18
+ const map = {};
19
+ for (const f of files) {
20
+ const p = path.join(ROOT, f);
21
+ if (!fs.existsSync(p)) continue;
22
+ const s = fs.readFileSync(p, "utf8");
23
+ const obj = /resolve\s*:\s*{[\s\S]*?alias\s*:\s*{([\s\S]*?)}/m.exec(s);
24
+ if (!obj) continue;
25
+ const re = /['"]([^'"]+)['"]\s*:\s*['"]([^'"]+)['"]/g;
26
+ let m;
27
+ while ((m = re.exec(obj[1]))) {
28
+ const key = m[1];
29
+ const raw = m[2].replace(/^\/+/, "");
30
+ const abs = path.resolve(ROOT, raw);
31
+ map[key] = abs;
32
+ }
33
+ }
34
+ return map;
35
+ }
36
+
37
+ function loadTsconfigAliases() {
38
+ const p = path.join(ROOT, "tsconfig.json");
39
+ if (!fs.existsSync(p)) return {};
40
+ let content = fs.readFileSync(p, "utf8")
41
+ .replace(/("(?:\\.|[^"\\])*")|\/\/.*$|\/\*[\s\S]*?\*\//gm, (_, q) => q || "")
42
+ .replace(/,\s*([}\]])/g, "$1");
43
+ let j;
44
+ try {
45
+ j = JSON.parse(content);
46
+ } catch {
47
+ return {};
48
+ }
49
+ const paths = j.compilerOptions?.paths || {};
50
+ const base = j.compilerOptions?.baseUrl || ".";
51
+ const map = {};
52
+ for (const k in paths) {
53
+ const arr = paths[k];
54
+ if (!Array.isArray(arr) || !arr.length) continue;
55
+ const from = k.replace(/\*$/, "");
56
+ const to = arr[0].replace(/\*$/, "");
57
+ map[from] = path.resolve(ROOT, base, to);
58
+ }
59
+ return map;
60
+ }
61
+
62
+ function loadJsAliases() {
63
+ return { ...loadTsconfigAliases(), ...loadViteAliases() };
64
+ }
65
+
66
+ // --- Resolver Core --------------------------------------------
67
+
68
+ function tryResolveImport(basePath) {
69
+ const ext = path.extname(basePath);
70
+ const tries = [];
71
+ if (ext) tries.push(basePath);
72
+ else {
73
+ for (const x of [".ts", ".tsx", ".d.ts", ".js", ".jsx", ".mjs"])
74
+ tries.push(basePath + x, path.join(basePath, "index" + x));
75
+ }
76
+ for (const t of tries)
77
+ if (fs.existsSync(t) && fs.statSync(t).isFile()) return path.resolve(t);
78
+ return null;
79
+ }
80
+
81
+ function isImportExcluded(p) {
82
+ return IMPORT_EXCLUDES.some(ex => p.includes(ex));
83
+ }
84
+
85
+ export async function resolveJsImports(filePath, visited = new Set(), depth = 0, maxDepth = 10) {
86
+ if (visited.has(filePath)) return { files: [], visited };
87
+ visited.add(filePath);
88
+ if (isImportExcluded(filePath) || !fs.existsSync(filePath))
89
+ return { files: [], visited };
90
+
91
+ const code = fs.readFileSync(filePath, "utf8");
92
+ const ext = path.extname(filePath).toLowerCase();
93
+ if (![".ts", ".tsx", ".d.ts", ".js", ".jsx", ".mjs"].includes(ext))
94
+ return { files: [], visited };
95
+
96
+ const aliases = loadJsAliases();
97
+ const patterns = [
98
+ /import\s+[^'"]*['"]([^'"]+)['"]/g,
99
+ /import\(\s*['"]([^'"]+)['"]\s*\)/g,
100
+ /require\(\s*['"]([^'"]+)['"]\s*\)/g,
101
+ /export\s+\*\s+from\s+['"]([^'"]+)['"]/g,
102
+ ];
103
+
104
+ const matches = new Set();
105
+ for (const r of patterns) {
106
+ let m;
107
+ while ((m = r.exec(code))) matches.add(m[1]);
108
+ }
109
+
110
+ const resolved = [];
111
+ for (const imp of matches) {
112
+ if (!imp.startsWith(".") && !imp.startsWith("/") && !imp.startsWith("@")) continue;
113
+ if (isImportExcluded(imp)) continue;
114
+
115
+ let importPath;
116
+ if (imp.startsWith("@")) {
117
+ const aliasKey = Object.keys(aliases).find(a => imp.startsWith(a));
118
+ if (aliasKey) {
119
+ const relPart = imp.slice(aliasKey.length).replace(/^\/+/, "");
120
+ importPath = path.join(aliases[aliasKey], relPart);
121
+ } else continue;
122
+ } else importPath = path.resolve(path.dirname(filePath), imp);
123
+
124
+ const resolvedPath = tryResolveImport(importPath);
125
+ if (!resolvedPath || isImportExcluded(resolvedPath)) continue;
126
+ resolved.push(resolvedPath);
127
+
128
+ if (depth < maxDepth) {
129
+ const sub = await resolveJsImports(resolvedPath, visited, depth + 1, maxDepth);
130
+ resolved.push(...sub.files);
131
+ }
132
+ }
133
+
134
+ return { files: [...new Set(resolved)], visited };
135
+ }
@@ -0,0 +1,31 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { ROOT } from "../constants/config.js";
4
+
5
+ export function loadLaravelBindings() {
6
+ const providersDir = path.join(ROOT, "app", "Providers");
7
+ const bindings = {};
8
+
9
+ if (!fs.existsSync(providersDir)) return bindings;
10
+
11
+ const files = fs
12
+ .readdirSync(providersDir)
13
+ .filter(f => f.endsWith(".php"))
14
+ .map(f => path.join(providersDir, f));
15
+
16
+ // Match: $this->app->bind(Interface::class, Implementation::class)
17
+ const re =
18
+ /\$this->app->(?:bind|singleton)\s*\(\s*([A-Za-z0-9_:\\\\]+)::class\s*,\s*([A-Za-z0-9_:\\\\]+)::class/g;
19
+
20
+ for (const file of files) {
21
+ const code = fs.readFileSync(file, "utf8");
22
+ let m;
23
+ while ((m = re.exec(code))) {
24
+ const iface = m[1].replace(/\\\\/g, "\\");
25
+ const impl = m[2].replace(/\\\\/g, "\\");
26
+ bindings[iface] = impl;
27
+ }
28
+ }
29
+
30
+ return bindings;
31
+ }
@@ -0,0 +1,115 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { ROOT } from "../constants/config.js";
4
+ import { loadLaravelBindings } from "./php-bindings.js";
5
+
6
+ const debug = process.env.PRODEX_DEBUG === "1";
7
+ const log = (...args) => { if (debug) console.log("🪶 [php-resolver]", ...args); };
8
+
9
+ // --- Load Composer PSR-4 Namespaces ----------------------------------------
10
+
11
+ function loadComposerNamespaces() {
12
+ const composerPath = path.join(ROOT, "composer.json");
13
+ if (!fs.existsSync(composerPath)) return {};
14
+ try {
15
+ const data = JSON.parse(fs.readFileSync(composerPath, "utf8"));
16
+ const psr4 = data.autoload?.["psr-4"] || {};
17
+ const map = {};
18
+ for (const ns in psr4)
19
+ map[ns.replace(/\\+$/, "")] = path.resolve(ROOT, psr4[ns]);
20
+ return map;
21
+ } catch {
22
+ return {};
23
+ }
24
+ }
25
+
26
+ // --- File resolver ---------------------------------------------------------
27
+
28
+ function tryResolvePhpImport(basePath) {
29
+ if (!basePath || typeof basePath !== "string") return null;
30
+ const tries = [basePath, basePath + ".php", path.join(basePath, "index.php")];
31
+ for (const t of tries)
32
+ if (fs.existsSync(t) && fs.statSync(t).isFile()) return path.resolve(t);
33
+ return null;
34
+ }
35
+
36
+ // --- Main resolver ---------------------------------------------------------
37
+
38
+ export async function resolvePhpImports(
39
+ filePath,
40
+ visited = new Set(),
41
+ depth = 0,
42
+ maxDepth = 10
43
+ ) {
44
+ if (visited.has(filePath)) return { files: [], visited };
45
+ visited.add(filePath);
46
+ if (!fs.existsSync(filePath)) return { files: [], visited };
47
+
48
+ const code = fs.readFileSync(filePath, "utf8");
49
+
50
+ // find include/require + grouped and single use statements
51
+ const patterns = [
52
+ /\b(?:require|include|require_once|include_once)\s*\(?['"]([^'"]+)['"]\)?/g,
53
+ /\buse\s+([A-Z][\w\\]+(?:\s*{[^}]+})?)/g,
54
+ ];
55
+
56
+ const rawMatches = new Set();
57
+ for (const r of patterns) {
58
+ let m;
59
+ while ((m = r.exec(code))) rawMatches.add(m[1]);
60
+ }
61
+
62
+ // Expand grouped uses
63
+ const matches = new Set();
64
+ for (const imp of rawMatches) {
65
+ const groupMatch = imp.match(/^(.+?)\s*{([^}]+)}/);
66
+ if (groupMatch) {
67
+ const base = groupMatch[1].trim().replace(/\\+$/, "");
68
+ const parts = groupMatch[2]
69
+ .split(",")
70
+ .map(x => x.trim())
71
+ .filter(Boolean);
72
+ for (const p of parts) matches.add(`${base}\\${p}`);
73
+ } else {
74
+ matches.add(imp.trim());
75
+ }
76
+ }
77
+
78
+ const namespaces = loadComposerNamespaces();
79
+ const bindings = loadLaravelBindings();
80
+ const resolved = [];
81
+
82
+ for (const imp0 of matches) {
83
+ let imp = imp0;
84
+
85
+ // Interface → Implementation mapping via Service Providers
86
+ if (bindings[imp]) {
87
+ imp = bindings[imp];
88
+ log("🔗 Interface resolved via AppServiceProvider:", imp0, "→", imp);
89
+ }
90
+
91
+ let importPath;
92
+
93
+ // PSR-4 namespace resolution
94
+ if (imp.includes("\\")) {
95
+ const nsKey = Object.keys(namespaces).find(k => imp.startsWith(k));
96
+ if (!nsKey) continue; // skip vendor namespaces
97
+ const relPart = imp.slice(nsKey.length).replace(/\\/g, "/");
98
+ importPath = path.join(namespaces[nsKey], `${relPart}.php`);
99
+ } else {
100
+ importPath = path.resolve(path.dirname(filePath), imp);
101
+ }
102
+
103
+ if (!importPath || typeof importPath !== "string") continue;
104
+ const resolvedPath = tryResolvePhpImport(importPath);
105
+ if (!resolvedPath) continue;
106
+ resolved.push(resolvedPath);
107
+
108
+ if (depth < maxDepth) {
109
+ const sub = await resolvePhpImports(resolvedPath, visited, depth + 1, maxDepth);
110
+ resolved.push(...sub.files);
111
+ }
112
+ }
113
+
114
+ return { files: [...new Set(resolved)], visited };
115
+ }