prodex 1.0.7 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,70 +1,70 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import { IMPORT_EXCLUDES, ROOT } from "../constants/config.js";
3
+ import micromatch from "micromatch";
4
+ import { safeMicromatchScan } from "../core/file-utils.js";
4
5
 
5
6
  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 = [
7
+ const log = (...a) => {
8
+ if (debug) console.log("ðŸŠķ [js-resolver]", ...a);
9
+ };
10
+
11
+ /**
12
+ * Load alias mappings from vite.config.* or tsconfig.json.
13
+ * Local config aliases (cfg.imports.aliases) override these.
14
+ */
15
+ function loadProjectAliases(ROOT) {
16
+ const viteFiles = [
12
17
  "vite.config.ts",
13
18
  "vite.config.js",
14
19
  "vite.config.mts",
15
20
  "vite.config.mjs",
16
- "vite.config.cjs",
21
+ "vite.config.cjs"
17
22
  ];
18
23
  const map = {};
19
- for (const f of files) {
24
+
25
+ // --- Vite aliases ---------------------------------------------------------
26
+ for (const f of viteFiles) {
20
27
  const p = path.join(ROOT, f);
21
28
  if (!fs.existsSync(p)) continue;
22
29
  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;
30
+ const block = /resolve\s*:\s*{[\s\S]*?alias\s*:\s*{([\s\S]*?)}/m.exec(s);
31
+ if (!block) continue;
25
32
  const re = /['"]([^'"]+)['"]\s*:\s*['"]([^'"]+)['"]/g;
26
33
  let m;
27
- while ((m = re.exec(obj[1]))) {
34
+ while ((m = re.exec(block[1]))) {
28
35
  const key = m[1];
29
36
  const raw = m[2].replace(/^\/+/, "");
30
- const abs = path.resolve(ROOT, raw);
31
- map[key] = abs;
37
+ map[key] = path.resolve(ROOT, raw);
32
38
  }
33
39
  }
34
- return map;
35
- }
36
40
 
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 {};
41
+ // --- TSConfig aliases -----------------------------------------------------
42
+ const ts = path.join(ROOT, "tsconfig.json");
43
+ if (fs.existsSync(ts)) {
44
+ try {
45
+ const content = fs
46
+ .readFileSync(ts, "utf8")
47
+ .replace(/("(?:\\.|[^"\\])*")|\/\/.*$|\/\*[\s\S]*?\*\//gm, (_, q) => q || "")
48
+ .replace(/,\s*([}\]])/g, "$1");
49
+ const j = JSON.parse(content);
50
+ const paths = j.compilerOptions?.paths || {};
51
+ const base = j.compilerOptions?.baseUrl || ".";
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
+ } catch {}
48
60
  }
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
61
 
62
- function loadJsAliases() {
63
- return { ...loadTsconfigAliases(), ...loadViteAliases() };
62
+ return map;
64
63
  }
65
64
 
66
- // --- Resolver Core --------------------------------------------
67
-
65
+ /**
66
+ * Try to resolve an import path into a real file.
67
+ */
68
68
  function tryResolveImport(basePath) {
69
69
  const ext = path.extname(basePath);
70
70
  const tries = [];
@@ -78,27 +78,49 @@ function tryResolveImport(basePath) {
78
78
  return null;
79
79
  }
80
80
 
81
- function isImportExcluded(p) {
82
- return IMPORT_EXCLUDES.some(ex => p.includes(ex));
83
- }
81
+ /**
82
+ * Core: resolve JS / TS imports recursively.
83
+ * Returns unique project-level expected + resolved import sets.
84
+ */
85
+ export async function resolveJsImports(
86
+ filePath,
87
+ cfg,
88
+ visited = new Set(),
89
+ depth = 0,
90
+ maxDepth = 10,
91
+ ctx = {}
92
+ ) {
93
+ if (depth >= maxDepth) {
94
+ if (debug) console.log(`⚠ïļ JS resolver depth (${maxDepth}) reached at ${filePath}`);
95
+ return { files: [], visited, stats: { expected: new Set(), resolved: new Set() } };
96
+ }
97
+
98
+ const { imports } = cfg;
99
+ const ROOT = process.cwd();
84
100
 
85
- export async function resolveJsImports(filePath, visited = new Set(), depth = 0, maxDepth = 10) {
86
- if (visited.has(filePath)) return { files: [], visited };
101
+ if (!ctx.aliases)
102
+ ctx.aliases = { ...loadProjectAliases(ROOT), ...imports.aliases };
103
+ const aliases = ctx.aliases;
104
+
105
+ const isExcluded = (p) =>
106
+ micromatch.isMatch(p.replaceAll("\\", "/"), imports.excludes || []);
107
+ if (visited.has(filePath))
108
+ return { files: [], visited, stats: { expected: new Set(), resolved: new Set() } };
87
109
  visited.add(filePath);
88
- if (isImportExcluded(filePath) || !fs.existsSync(filePath))
89
- return { files: [], visited };
110
+
111
+ if (!fs.existsSync(filePath) || isExcluded(filePath))
112
+ return { files: [], visited, stats: { expected: new Set(), resolved: new Set() } };
90
113
 
91
114
  const code = fs.readFileSync(filePath, "utf8");
92
115
  const ext = path.extname(filePath).toLowerCase();
93
116
  if (![".ts", ".tsx", ".d.ts", ".js", ".jsx", ".mjs"].includes(ext))
94
- return { files: [], visited };
117
+ return { files: [], visited, stats: { expected: new Set(), resolved: new Set() } };
95
118
 
96
- const aliases = loadJsAliases();
97
119
  const patterns = [
98
120
  /import\s+[^'"]*['"]([^'"]+)['"]/g,
99
121
  /import\(\s*['"]([^'"]+)['"]\s*\)/g,
100
122
  /require\(\s*['"]([^'"]+)['"]\s*\)/g,
101
- /export\s+\*\s+from\s+['"]([^'"]+)['"]/g,
123
+ /export\s+\*\s+from\s+['"]([^'"]+)['"]/g
102
124
  ];
103
125
 
104
126
  const matches = new Set();
@@ -108,28 +130,51 @@ export async function resolveJsImports(filePath, visited = new Set(), depth = 0,
108
130
  }
109
131
 
110
132
  const resolved = [];
133
+ const expectedImports = new Set();
134
+ const resolvedImports = new Set();
135
+
111
136
  for (const imp of matches) {
112
137
  if (!imp.startsWith(".") && !imp.startsWith("/") && !imp.startsWith("@")) continue;
113
- if (isImportExcluded(imp)) continue;
138
+ if (isExcluded(imp)) continue;
139
+
140
+ expectedImports.add(imp);
114
141
 
115
142
  let importPath;
116
143
  if (imp.startsWith("@")) {
117
- const aliasKey = Object.keys(aliases).find(a => imp.startsWith(a));
144
+ const aliasKey = Object.keys(aliases).find((a) => imp.startsWith(a));
118
145
  if (aliasKey) {
119
146
  const relPart = imp.slice(aliasKey.length).replace(/^\/+/, "");
120
147
  importPath = path.join(aliases[aliasKey], relPart);
121
148
  } else continue;
122
- } else importPath = path.resolve(path.dirname(filePath), imp);
149
+ } else {
150
+ importPath = path.resolve(path.dirname(filePath), imp);
151
+ }
123
152
 
124
153
  const resolvedPath = tryResolveImport(importPath);
125
- if (!resolvedPath || isImportExcluded(resolvedPath)) continue;
154
+ if (!resolvedPath || isExcluded(resolvedPath)) continue;
126
155
  resolved.push(resolvedPath);
156
+ resolvedImports.add(resolvedPath);
127
157
 
128
158
  if (depth < maxDepth) {
129
- const sub = await resolveJsImports(resolvedPath, visited, depth + 1, maxDepth);
159
+ const sub = await resolveJsImports(resolvedPath, cfg, visited, depth + 1, maxDepth, ctx);
130
160
  resolved.push(...sub.files);
161
+ sub.stats?.expected?.forEach((i) => expectedImports.add(i));
162
+ sub.stats?.resolved?.forEach((i) => resolvedImports.add(i));
163
+ }
164
+ }
165
+
166
+ // Always-include patterns (no recursion)
167
+ for (const pattern of imports.includes || []) {
168
+ const scan = safeMicromatchScan(pattern, { cwd: ROOT, absolute: true });
169
+ if (scan?.files) {
170
+ for (const f of scan.files) resolved.push(path.resolve(ROOT, f));
131
171
  }
132
172
  }
133
173
 
134
- return { files: [...new Set(resolved)], visited };
174
+ log("✅ JS resolver completed for", filePath, "→", resolved.length, "files");
175
+ return {
176
+ files: [...new Set(resolved)],
177
+ visited,
178
+ stats: { expected: expectedImports, resolved: resolvedImports }
179
+ };
135
180
  }
@@ -1,14 +1,18 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import { ROOT } from "../constants/config.js";
3
+ import micromatch from "micromatch";
4
4
  import { loadLaravelBindings } from "./php-bindings.js";
5
+ import { safeMicromatchScan } from "../core/file-utils.js";
5
6
 
6
7
  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() {
8
+ const log = (...a) => {
9
+ if (debug) console.log("ðŸŠķ [php-resolver]", ...a);
10
+ };
11
+
12
+ /**
13
+ * Load PSR-4 namespaces from composer.json.
14
+ */
15
+ function loadComposerNamespaces(ROOT) {
12
16
  const composerPath = path.join(ROOT, "composer.json");
13
17
  if (!fs.existsSync(composerPath)) return {};
14
18
  try {
@@ -23,8 +27,9 @@ function loadComposerNamespaces() {
23
27
  }
24
28
  }
25
29
 
26
- // --- File resolver ---------------------------------------------------------
27
-
30
+ /**
31
+ * Try to resolve a PHP include/namespace to a file.
32
+ */
28
33
  function tryResolvePhpImport(basePath) {
29
34
  if (!basePath || typeof basePath !== "string") return null;
30
35
  const tries = [basePath, basePath + ".php", path.join(basePath, "index.php")];
@@ -33,24 +38,49 @@ function tryResolvePhpImport(basePath) {
33
38
  return null;
34
39
  }
35
40
 
36
- // --- Main resolver ---------------------------------------------------------
37
-
41
+ /**
42
+ * Core PHP resolver — PSR-4 + Laravel bindings + glob excludes.
43
+ * Returns unique expected + resolved import sets.
44
+ */
38
45
  export async function resolvePhpImports(
39
46
  filePath,
47
+ cfg,
40
48
  visited = new Set(),
41
49
  depth = 0,
42
- maxDepth = 10
50
+ maxDepth = 10,
51
+ ctx = {}
43
52
  ) {
44
- if (visited.has(filePath)) return { files: [], visited };
53
+ if (depth >= maxDepth) {
54
+ if (debug) console.log(`⚠ïļ PHP resolver depth (${maxDepth}) reached at ${filePath}`);
55
+ return { files: [], visited, stats: { expected: new Set(), resolved: new Set() } };
56
+ }
57
+
58
+ const { imports } = cfg;
59
+ const ROOT = process.cwd();
60
+
61
+ if (!ctx.namespaces) ctx.namespaces = loadComposerNamespaces(ROOT);
62
+ if (!ctx.bindings) ctx.bindings = loadLaravelBindings();
63
+
64
+ const namespaces = ctx.namespaces;
65
+ const bindings = ctx.bindings;
66
+ const isExcluded = (p) =>
67
+ micromatch.isMatch(p.replaceAll("\\", "/"), imports.excludes || []);
68
+
69
+ if (visited.has(filePath))
70
+ return { files: [], visited, stats: { expected: new Set(), resolved: new Set() } };
45
71
  visited.add(filePath);
46
- if (!fs.existsSync(filePath)) return { files: [], visited };
72
+
73
+ if (!fs.existsSync(filePath) || isExcluded(filePath))
74
+ return { files: [], visited, stats: { expected: new Set(), resolved: new Set() } };
47
75
 
48
76
  const code = fs.readFileSync(filePath, "utf8");
77
+ const resolved = [];
78
+ const expectedImports = new Set();
79
+ const resolvedImports = new Set();
49
80
 
50
- // find include/require + grouped and single use statements
51
81
  const patterns = [
52
82
  /\b(?:require|include|require_once|include_once)\s*\(?['"]([^'"]+)['"]\)?/g,
53
- /\buse\s+([A-Z][\w\\]+(?:\s*{[^}]+})?)/g,
83
+ /\buse\s+([A-Z][\w\\]+(?:\s*{[^}]+})?)/g
54
84
  ];
55
85
 
56
86
  const rawMatches = new Set();
@@ -67,49 +97,59 @@ export async function resolvePhpImports(
67
97
  const base = groupMatch[1].trim().replace(/\\+$/, "");
68
98
  const parts = groupMatch[2]
69
99
  .split(",")
70
- .map(x => x.trim())
100
+ .map((x) => x.trim())
71
101
  .filter(Boolean);
72
- for (const p of parts) matches.add(`${base}\${p}`);
102
+ for (const p of parts) matches.add(`${base}\\${p}`);
73
103
  } else {
74
104
  matches.add(imp.trim());
75
105
  }
76
106
  }
77
107
 
78
- const namespaces = loadComposerNamespaces();
79
- const bindings = loadLaravelBindings();
80
- const resolved = [];
81
-
82
108
  for (const imp0 of matches) {
83
109
  let imp = imp0;
84
110
 
85
- // Interface → Implementation mapping via Service Providers
86
111
  if (bindings[imp]) {
87
112
  imp = bindings[imp];
88
- log("🔗 Interface resolved via AppServiceProvider:", imp0, "→", imp);
113
+ log("🔗 Interface resolved via binding:", imp0, "→", imp);
89
114
  }
90
115
 
91
116
  let importPath;
92
-
93
- // PSR-4 namespace resolution
94
117
  if (imp.includes("\\")) {
95
- const nsKey = Object.keys(namespaces).find(k => imp.startsWith(k));
96
- if (!nsKey) continue; // skip vendor namespaces
118
+ const nsKey = Object.keys(namespaces).find((k) => imp.startsWith(k));
119
+ if (!nsKey) continue;
97
120
  const relPart = imp.slice(nsKey.length).replace(/\\/g, "/");
98
121
  importPath = path.join(namespaces[nsKey], `${relPart}.php`);
99
122
  } else {
100
123
  importPath = path.resolve(path.dirname(filePath), imp);
101
124
  }
102
125
 
103
- if (!importPath || typeof importPath !== "string") continue;
126
+ if (!importPath || isExcluded(importPath)) continue;
127
+
128
+ expectedImports.add(imp);
129
+
104
130
  const resolvedPath = tryResolvePhpImport(importPath);
105
- if (!resolvedPath) continue;
131
+ if (!resolvedPath || isExcluded(resolvedPath)) continue;
106
132
  resolved.push(resolvedPath);
133
+ resolvedImports.add(resolvedPath);
107
134
 
108
135
  if (depth < maxDepth) {
109
- const sub = await resolvePhpImports(resolvedPath, visited, depth + 1, maxDepth);
136
+ const sub = await resolvePhpImports(resolvedPath, cfg, visited, depth + 1, maxDepth, ctx);
110
137
  resolved.push(...sub.files);
138
+ sub.stats?.expected?.forEach((i) => expectedImports.add(i));
139
+ sub.stats?.resolved?.forEach((i) => resolvedImports.add(i));
111
140
  }
112
141
  }
113
142
 
114
- return { files: [...new Set(resolved)], visited };
143
+ // Always-include patterns (no recursion)
144
+ for (const pattern of imports.includes || []) {
145
+ const scan = safeMicromatchScan(pattern, { cwd: ROOT, absolute: true });
146
+ if (scan?.files) for (const f of scan.files) resolved.push(path.resolve(ROOT, f));
147
+ }
148
+
149
+ log("✅ PHP resolver completed for", filePath, "→", resolved.length, "files");
150
+ return {
151
+ files: [...new Set(resolved)],
152
+ visited,
153
+ stats: { expected: expectedImports, resolved: resolvedImports }
154
+ };
115
155
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prodex",
3
- "version": "1.0.7",
3
+ "version": "1.1.0",
4
4
  "description": "Unified Project Indexer & Dependency Extractor for Laravel + React + Node stacks.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,10 +34,12 @@
34
34
  "author": "emxhive",
35
35
  "license": "MIT",
36
36
  "devDependencies": {
37
+ "@types/node": "^24.9.1",
37
38
  "tsup": "^8.5.0",
38
39
  "typescript": "^5.9.3"
39
40
  },
40
41
  "dependencies": {
41
- "inquirer": "^12.10.0"
42
+ "inquirer": "^12.10.0",
43
+ "micromatch": "^4.0.8"
42
44
  }
43
45
  }