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 +21 -0
- package/README.md +197 -0
- package/bin/prodex.js +2 -0
- package/package.json +18 -0
- package/src/cli/init.js +49 -0
- package/src/cli/picker.js +8 -0
- package/src/cli/summary.js +4 -0
- package/src/constants/config-loader.js +47 -0
- package/src/constants/config.js +26 -0
- package/src/core/alias-loader.js +8 -0
- package/src/core/combine.js +129 -0
- package/src/core/exclusions.js +23 -0
- package/src/core/file-utils.js +19 -0
- package/src/core/helpers.js +51 -0
- package/src/index.js +11 -0
- package/src/resolvers/js-resolver.js +135 -0
- package/src/resolvers/php-bindings.js +31 -0
- package/src/resolvers/php-resolver.js +115 -0
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
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
|
+
}
|
package/src/cli/init.js
ADDED
|
@@ -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,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,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
|
+
}
|