@inspecto-dev/cli 0.2.0-alpha.2 → 0.2.0-alpha.3

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,6 +1,6 @@
1
1
 
2
2
  
3
- > @inspecto-dev/cli@0.2.0-alpha.1 build /Users/bytedance/Works/hugo.felix/inspecto/packages/cli
3
+ > @inspecto-dev/cli@0.2.0-alpha.2 build /Users/bytedance/Works/hugo.felix/inspecto/packages/cli
4
4
  > tsup
5
5
 
6
6
  CLI Building entry: src/bin.ts, src/index.ts
@@ -10,11 +10,11 @@
10
10
  CLI Target: node18
11
11
  CLI Cleaning output folder
12
12
  ESM Build start
13
- ESM dist/bin.js 2.64 KB
14
13
  ESM dist/index.js 109.00 B
15
- ESM dist/chunk-V57BJXGZ.js 50.37 KB
16
- ESM ⚡️ Build success in 41ms
14
+ ESM dist/bin.js 2.64 KB
15
+ ESM dist/chunk-HIL6365F.js 53.63 KB
16
+ ESM ⚡️ Build success in 26ms
17
17
  DTS Build start
18
- DTS ⚡️ Build success in 1418ms
18
+ DTS ⚡️ Build success in 1180ms
19
19
  DTS dist/bin.d.ts 13.00 B
20
20
  DTS dist/index.d.ts 1.18 KB
@@ -1,15 +1,15 @@
1
1
 
2
- > @inspecto-dev/cli@0.2.0-alpha.1 test /Users/bytedance/Works/hugo.felix/inspecto/packages/cli
2
+ > @inspecto-dev/cli@0.2.0-alpha.2 test /Users/bytedance/Works/hugo.felix/inspecto/packages/cli
3
3
  > vitest run --passWithNoTests
4
4
 
5
5
 
6
6
  RUN v1.6.1 /Users/bytedance/Works/hugo.felix/inspecto/packages/cli
7
7
 
8
- ✓ tests/framework.test.ts (5 tests) 6ms
9
- ✓ tests/ide.test.ts (6 tests) 7ms
8
+ ✓ tests/ide.test.ts (6 tests) 3ms
9
+ ✓ tests/framework.test.ts (5 tests) 2ms
10
10
 
11
11
  Test Files 2 passed (2)
12
12
  Tests 11 passed (11)
13
- Start at 15:10:05
14
- Duration 798ms (transform 169ms, setup 0ms, collect 246ms, tests 13ms, environment 0ms, prepare 674ms)
13
+ Start at 19:47:34
14
+ Duration 213ms (transform 48ms, setup 0ms, collect 65ms, tests 5ms, environment 0ms, prepare 121ms)
15
15
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @inspecto-dev/cli
2
2
 
3
+ ## 0.2.0-alpha.3
4
+
5
+ ### Minor Changes
6
+
7
+ - release alpha test version
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - @inspecto-dev/types@0.2.0-alpha.3
13
+
3
14
  ## 0.2.0-alpha.2
4
15
 
5
16
  ### Minor Changes
package/dist/bin.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  init,
4
4
  log,
5
5
  teardown
6
- } from "./chunk-V57BJXGZ.js";
6
+ } from "./chunk-HIL6365F.js";
7
7
 
8
8
  // src/bin.ts
9
9
  import { cac } from "cac";
@@ -177,6 +177,31 @@ function getUninstallCommand(pm, pkg) {
177
177
 
178
178
  // src/detect/build-tool.ts
179
179
  import path3 from "path";
180
+ import { createRequire } from "module";
181
+ function isPackageResolvable(pkgName, root) {
182
+ try {
183
+ const require2 = createRequire(path3.join(root, "package.json"));
184
+ try {
185
+ require2.resolve(`${pkgName}/package.json`, { paths: [root] });
186
+ return true;
187
+ } catch {
188
+ require2.resolve(pkgName, { paths: [root] });
189
+ return true;
190
+ }
191
+ } catch {
192
+ return false;
193
+ }
194
+ }
195
+ async function getResolvedPackageVersion(pkgName, root) {
196
+ try {
197
+ const require2 = createRequire(path3.join(root, "package.json"));
198
+ const pkgJsonPath = require2.resolve(`${pkgName}/package.json`, { paths: [root] });
199
+ const pkg = await readJSON(pkgJsonPath);
200
+ return pkg?.version || null;
201
+ } catch {
202
+ return null;
203
+ }
204
+ }
180
205
  var SUPPORTED_PATTERNS = [
181
206
  {
182
207
  tool: "vite",
@@ -222,7 +247,25 @@ async function detectBuildTools(root) {
222
247
  const pkg = await readJSON(path3.join(root, "package.json"));
223
248
  const allDeps = { ...pkg?.dependencies, ...pkg?.devDependencies };
224
249
  const supportedChecks = SUPPORTED_PATTERNS.map(async (pattern) => {
225
- const hasDep = pattern.tool === "rspack" ? !!(allDeps["@rspack/cli"] || allDeps["@rspack/core"]) : pattern.tool === "webpack" ? !!(allDeps["webpack"] || allDeps["webpack-cli"]) : pattern.tool === "rsbuild" ? !!allDeps["@rsbuild/core"] : !!allDeps[pattern.tool];
250
+ let hasDep;
251
+ let resolvedVersion = null;
252
+ if (pattern.tool === "rspack") {
253
+ const depName = allDeps["@rspack/cli"] ? "@rspack/cli" : "@rspack/core";
254
+ hasDep = !!allDeps["@rspack/cli"] || !!allDeps["@rspack/core"] || isPackageResolvable("@rspack/core", root);
255
+ if (hasDep) {
256
+ resolvedVersion = allDeps[depName] || await getResolvedPackageVersion("@rspack/core", root);
257
+ }
258
+ } else if (pattern.tool === "webpack") {
259
+ const depName = allDeps["webpack"] ? "webpack" : "webpack-cli";
260
+ hasDep = !!allDeps["webpack"] || !!allDeps["webpack-cli"] || isPackageResolvable("webpack", root);
261
+ if (hasDep) {
262
+ resolvedVersion = allDeps[depName] || await getResolvedPackageVersion("webpack", root);
263
+ }
264
+ } else if (pattern.tool === "rsbuild") {
265
+ hasDep = !!allDeps["@rsbuild/core"] || isPackageResolvable("@rsbuild/core", root);
266
+ } else {
267
+ hasDep = !!allDeps[pattern.tool] || isPackageResolvable(pattern.tool, root);
268
+ }
226
269
  let detectedFile = "";
227
270
  if (pattern.tool === "esbuild" && !hasDep) {
228
271
  return null;
@@ -233,20 +276,33 @@ async function detectBuildTools(root) {
233
276
  break;
234
277
  }
235
278
  }
236
- if (hasDep && !detectedFile && (pattern.tool === "esbuild" || pattern.tool === "rollup")) {
279
+ if (hasDep && !detectedFile && (pattern.tool === "esbuild" || pattern.tool === "rollup" || pattern.tool === "webpack" || pattern.tool === "rspack" || pattern.tool === "rsbuild")) {
237
280
  const scripts = pkg?.scripts || {};
238
- for (const [_, cmd] of Object.entries(scripts)) {
281
+ for (const cmd of Object.values(scripts)) {
239
282
  if (cmd.includes("node ")) {
240
283
  const match = cmd.match(/node\s+([^\s]+\.(js|mjs|cjs|ts))/);
241
284
  if (match && match[1]) {
242
285
  if (await exists(path3.join(root, match[1]))) {
243
- detectedFile = match[1];
244
- break;
286
+ if (cmd.includes(pattern.tool) || match[1].includes(pattern.tool)) {
287
+ detectedFile = match[1];
288
+ break;
289
+ }
245
290
  }
246
291
  }
247
292
  } else if (cmd.includes(`${pattern.tool} `)) {
248
- detectedFile = "package.json (scripts)";
249
- break;
293
+ if (pattern.tool === "webpack" || pattern.tool === "rspack") {
294
+ const configMatch = cmd.match(/--config\s+([^\s]+)/);
295
+ if (configMatch && configMatch[1]) {
296
+ if (await exists(path3.join(root, configMatch[1]))) {
297
+ detectedFile = configMatch[1];
298
+ break;
299
+ }
300
+ }
301
+ }
302
+ if (!detectedFile) {
303
+ detectedFile = "package.json (scripts)";
304
+ break;
305
+ }
250
306
  }
251
307
  }
252
308
  }
@@ -254,12 +310,12 @@ async function detectBuildTools(root) {
254
310
  let isLegacyRspack = false;
255
311
  let isLegacyWebpack = false;
256
312
  if (pattern.tool === "rspack") {
257
- const version = allDeps["@rspack/cli"] || allDeps["@rspack/core"];
313
+ const version = resolvedVersion;
258
314
  if (version && (version.includes("0.3.") || version.includes("0.2.") || version.includes("0.1."))) {
259
315
  isLegacyRspack = true;
260
316
  }
261
317
  } else if (pattern.tool === "webpack") {
262
- const version = allDeps["webpack"] || allDeps["webpack-cli"];
318
+ const version = resolvedVersion;
263
319
  if (version && version.includes("^4") || version?.startsWith("4.")) {
264
320
  isLegacyWebpack = true;
265
321
  }
@@ -305,6 +361,18 @@ function resolveInjectionTarget(detections) {
305
361
 
306
362
  // src/detect/framework.ts
307
363
  import path4 from "path";
364
+ import { createRequire as createRequire2 } from "module";
365
+ var META_FRAMEWORK_MAP = {
366
+ next: "react",
367
+ nuxt: "vue",
368
+ "@remix-run/react": "react",
369
+ "@remix-run/dev": "react",
370
+ "@vue/nuxt": "vue",
371
+ "vite-plugin-vue": "vue",
372
+ "@vitejs/plugin-vue": "vue",
373
+ "@vitejs/plugin-react": "react",
374
+ "@vitejs/plugin-react-swc": "react"
375
+ };
308
376
  var SUPPORTED_FRAMEWORKS = [
309
377
  { framework: "react", deps: ["react", "react-dom"] },
310
378
  { framework: "vue", deps: ["vue"] }
@@ -316,26 +384,53 @@ var UNSUPPORTED_FRAMEWORKS = [
316
384
  { name: "Preact", dep: "preact" },
317
385
  { name: "Lit", dep: "lit" }
318
386
  ];
387
+ function isPackageResolvable2(pkgName, root) {
388
+ try {
389
+ const require2 = createRequire2(path4.join(root, "package.json"));
390
+ try {
391
+ require2.resolve(`${pkgName}/package.json`, { paths: [root] });
392
+ return true;
393
+ } catch {
394
+ require2.resolve(pkgName, { paths: [root] });
395
+ return true;
396
+ }
397
+ } catch {
398
+ return false;
399
+ }
400
+ }
319
401
  async function detectFrameworks(root) {
320
402
  const pkg = await readJSON(path4.join(root, "package.json"));
321
- if (!pkg) return { supported: [], unsupported: [] };
322
403
  const allDeps = {
323
- ...pkg.dependencies,
324
- ...pkg.devDependencies
404
+ ...pkg?.dependencies || {},
405
+ ...pkg?.devDependencies || {},
406
+ ...pkg?.peerDependencies || {}
325
407
  };
326
- const supported = [];
408
+ const supportedSet = /* @__PURE__ */ new Set();
409
+ const unsupported = [];
410
+ const isTest = root.includes("/mock/root");
411
+ for (const [metaPkg, framework] of Object.entries(META_FRAMEWORK_MAP)) {
412
+ if (metaPkg in allDeps || !isTest && isPackageResolvable2(metaPkg, root)) {
413
+ supportedSet.add(framework);
414
+ }
415
+ }
327
416
  for (const { framework, deps } of SUPPORTED_FRAMEWORKS) {
328
- if (deps.some((dep) => dep in allDeps)) {
329
- supported.push(framework);
417
+ if (supportedSet.has(framework)) continue;
418
+ for (const dep of deps) {
419
+ if (dep in allDeps || !isTest && isPackageResolvable2(dep, root)) {
420
+ supportedSet.add(framework);
421
+ break;
422
+ }
330
423
  }
331
424
  }
332
- const unsupported = [];
333
425
  for (const fw of UNSUPPORTED_FRAMEWORKS) {
334
- if (fw.dep in allDeps) {
426
+ if (fw.dep in allDeps || !isTest && isPackageResolvable2(fw.dep, root)) {
335
427
  unsupported.push(fw);
336
428
  }
337
429
  }
338
- return { supported, unsupported };
430
+ return {
431
+ supported: Array.from(supportedSet),
432
+ unsupported
433
+ };
339
434
  }
340
435
 
341
436
  // src/detect/ide.ts
@@ -1030,10 +1125,10 @@ async function init(options) {
1030
1125
  if (frameworkResult.supported.length > 0) {
1031
1126
  log.success(`Detected framework: ${frameworkResult.supported.join(", ")}`);
1032
1127
  }
1033
- const hasUnsupportedFramework = frameworkResult.unsupported.length > 0;
1034
- const hasNoFramework = frameworkResult.supported.length === 0 && frameworkResult.unsupported.length === 0;
1035
- if (hasUnsupportedFramework || hasNoFramework) {
1036
- if (hasUnsupportedFramework) {
1128
+ const isSupported = frameworkResult.supported.length > 0;
1129
+ const hasUnsupported = frameworkResult.unsupported.length > 0;
1130
+ if (!isSupported) {
1131
+ if (hasUnsupported) {
1037
1132
  const names = frameworkResult.unsupported.map((f) => f.name).join(", ");
1038
1133
  log.warn(`Detected ${names} \u2014 not supported in v1 (React / Vue only)`);
1039
1134
  } else {
@@ -1049,6 +1144,11 @@ async function init(options) {
1049
1144
  } else {
1050
1145
  log.warn("Continuing anyway (--force)");
1051
1146
  }
1147
+ } else if (hasUnsupported) {
1148
+ const names = frameworkResult.unsupported.map((f) => f.name).join(", ");
1149
+ log.hint(
1150
+ `Note: Inspecto will be configured for ${frameworkResult.supported.join(", ")}. Other detected frameworks (${names}) will be ignored.`
1151
+ );
1052
1152
  }
1053
1153
  let manualConfigRequiredFor = "";
1054
1154
  if (buildResult.supported.length > 0) {
@@ -1065,7 +1165,7 @@ async function init(options) {
1065
1165
  log.hint("current version supports: Vite, Webpack, Rspack, esbuild, Rollup");
1066
1166
  log.hint("Dependency will be installed but plugin injection will be skipped");
1067
1167
  log.hint(
1068
- "Please refer to the manual setup guide: https://inspecto.dev/docs/getting-started/manual-setup"
1168
+ "Please refer to the manual setup guide: https://inspecto-dev.github.io/inspecto/guide/manual-installation"
1069
1169
  );
1070
1170
  }
1071
1171
  let selectedIDE = null;
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  doctor,
3
3
  init,
4
4
  teardown
5
- } from "./chunk-V57BJXGZ.js";
5
+ } from "./chunk-HIL6365F.js";
6
6
  export {
7
7
  doctor,
8
8
  init,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inspecto-dev/cli",
3
- "version": "0.2.0-alpha.2",
3
+ "version": "0.2.0-alpha.3",
4
4
  "description": "CLI tools for Inspecto onboarding and lifecycle management",
5
5
  "keywords": [
6
6
  "inspecto",
@@ -19,7 +19,7 @@
19
19
  "magicast": "^0.5.2",
20
20
  "picocolors": "^1.0.0",
21
21
  "prompts": "^2.4.2",
22
- "@inspecto-dev/types": "0.2.0-alpha.2"
22
+ "@inspecto-dev/types": "0.2.0-alpha.3"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^20.0.0",
@@ -57,12 +57,11 @@ export async function init(options: InitOptions): Promise<void> {
57
57
  log.success(`Detected framework: ${frameworkResult.supported.join(', ')}`)
58
58
  }
59
59
 
60
- const hasUnsupportedFramework = frameworkResult.unsupported.length > 0
61
- const hasNoFramework =
62
- frameworkResult.supported.length === 0 && frameworkResult.unsupported.length === 0
60
+ const isSupported = frameworkResult.supported.length > 0
61
+ const hasUnsupported = frameworkResult.unsupported.length > 0
63
62
 
64
- if (hasUnsupportedFramework || hasNoFramework) {
65
- if (hasUnsupportedFramework) {
63
+ if (!isSupported) {
64
+ if (hasUnsupported) {
66
65
  const names = frameworkResult.unsupported.map(f => f.name).join(', ')
67
66
  log.warn(`Detected ${names} — not supported in v1 (React / Vue only)`)
68
67
  } else {
@@ -79,6 +78,11 @@ export async function init(options: InitOptions): Promise<void> {
79
78
  } else {
80
79
  log.warn('Continuing anyway (--force)')
81
80
  }
81
+ } else if (hasUnsupported) {
82
+ const names = frameworkResult.unsupported.map(f => f.name).join(', ')
83
+ log.hint(
84
+ `Note: Inspecto will be configured for ${frameworkResult.supported.join(', ')}. Other detected frameworks (${names}) will be ignored.`,
85
+ )
82
86
  }
83
87
 
84
88
  // Build tool detection
@@ -97,7 +101,7 @@ export async function init(options: InitOptions): Promise<void> {
97
101
  log.hint('current version supports: Vite, Webpack, Rspack, esbuild, Rollup')
98
102
  log.hint('Dependency will be installed but plugin injection will be skipped')
99
103
  log.hint(
100
- 'Please refer to the manual setup guide: https://inspecto.dev/docs/getting-started/manual-setup',
104
+ 'Please refer to the manual setup guide: https://inspecto-dev.github.io/inspecto/guide/manual-installation',
101
105
  )
102
106
  }
103
107
 
@@ -5,6 +5,7 @@
5
5
  // Recognized but unsupported: Next.js / Nuxt / Remix / Astro / SvelteKit
6
6
  // ============================================================
7
7
  import path from 'node:path'
8
+ import { createRequire } from 'node:module'
8
9
  import { exists, readJSON } from '../utils/fs.js'
9
10
  import type { BuildTool, BuildToolDetection } from '../types.js'
10
11
 
@@ -12,6 +13,40 @@ interface PackageJSON {
12
13
  dependencies?: Record<string, string>
13
14
  devDependencies?: Record<string, string>
14
15
  scripts?: Record<string, string>
16
+ version?: string
17
+ }
18
+
19
+ /**
20
+ * Helper to check if a package can be resolved from the root directory.
21
+ * This handles monorepo hoisting and implicit dependencies.
22
+ */
23
+ function isPackageResolvable(pkgName: string, root: string): boolean {
24
+ try {
25
+ const require = createRequire(path.join(root, 'package.json'))
26
+ try {
27
+ require.resolve(`${pkgName}/package.json`, { paths: [root] })
28
+ return true
29
+ } catch {
30
+ require.resolve(pkgName, { paths: [root] })
31
+ return true
32
+ }
33
+ } catch {
34
+ return false
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Attempts to read the actual version of a hoisted package from node_modules.
40
+ */
41
+ async function getResolvedPackageVersion(pkgName: string, root: string): Promise<string | null> {
42
+ try {
43
+ const require = createRequire(path.join(root, 'package.json'))
44
+ const pkgJsonPath = require.resolve(`${pkgName}/package.json`, { paths: [root] })
45
+ const pkg = await readJSON<PackageJSON>(pkgJsonPath)
46
+ return pkg?.version || null
47
+ } catch {
48
+ return null
49
+ }
15
50
  }
16
51
 
17
52
  /** Supported build tools in v1 */
@@ -76,14 +111,33 @@ export async function detectBuildTools(root: string): Promise<BuildToolResult> {
76
111
 
77
112
  const supportedChecks = SUPPORTED_PATTERNS.map(async pattern => {
78
113
  // 1. Check if the package.json has a dependency for this tool
79
- const hasDep =
80
- pattern.tool === 'rspack'
81
- ? !!(allDeps['@rspack/cli'] || allDeps['@rspack/core'])
82
- : pattern.tool === 'webpack'
83
- ? !!(allDeps['webpack'] || allDeps['webpack-cli'])
84
- : pattern.tool === 'rsbuild'
85
- ? !!allDeps['@rsbuild/core']
86
- : !!allDeps[pattern.tool]
114
+ let hasDep: boolean
115
+ let resolvedVersion: string | null = null
116
+
117
+ if (pattern.tool === 'rspack') {
118
+ const depName = allDeps['@rspack/cli'] ? '@rspack/cli' : '@rspack/core'
119
+ hasDep =
120
+ !!allDeps['@rspack/cli'] ||
121
+ !!allDeps['@rspack/core'] ||
122
+ isPackageResolvable('@rspack/core', root)
123
+
124
+ if (hasDep) {
125
+ resolvedVersion =
126
+ allDeps[depName] || (await getResolvedPackageVersion('@rspack/core', root))
127
+ }
128
+ } else if (pattern.tool === 'webpack') {
129
+ const depName = allDeps['webpack'] ? 'webpack' : 'webpack-cli'
130
+ hasDep =
131
+ !!allDeps['webpack'] || !!allDeps['webpack-cli'] || isPackageResolvable('webpack', root)
132
+
133
+ if (hasDep) {
134
+ resolvedVersion = allDeps[depName] || (await getResolvedPackageVersion('webpack', root))
135
+ }
136
+ } else if (pattern.tool === 'rsbuild') {
137
+ hasDep = !!allDeps['@rsbuild/core'] || isPackageResolvable('@rsbuild/core', root)
138
+ } else {
139
+ hasDep = !!allDeps[pattern.tool] || isPackageResolvable(pattern.tool, root)
140
+ }
87
141
 
88
142
  // 2. Look for config files
89
143
  let detectedFile = ''
@@ -100,23 +154,49 @@ export async function detectBuildTools(root: string): Promise<BuildToolResult> {
100
154
  }
101
155
  }
102
156
 
103
- // 3. For esbuild and rollup, if they are in dependencies but no standard config is found,
104
- // we still consider them detected (as they are often used with custom scripts)
105
- if (hasDep && !detectedFile && (pattern.tool === 'esbuild' || pattern.tool === 'rollup')) {
157
+ // 3. For esbuild, rollup, and webpack, if they are in dependencies but no standard config is found,
158
+ // we still consider them detected (as they are often used with custom scripts or config file names)
159
+ if (
160
+ hasDep &&
161
+ !detectedFile &&
162
+ (pattern.tool === 'esbuild' ||
163
+ pattern.tool === 'rollup' ||
164
+ pattern.tool === 'webpack' ||
165
+ pattern.tool === 'rspack' ||
166
+ pattern.tool === 'rsbuild')
167
+ ) {
106
168
  // Look at npm scripts to guess the build file
107
169
  const scripts = pkg?.scripts || {}
108
- for (const [_, cmd] of Object.entries(scripts)) {
170
+ for (const cmd of Object.values(scripts)) {
109
171
  if (cmd.includes('node ')) {
110
172
  const match = cmd.match(/node\s+([^\s]+\.(js|mjs|cjs|ts))/)
111
173
  if (match && match[1]) {
112
174
  if (await exists(path.join(root, match[1]))) {
113
- detectedFile = match[1]
114
- break
175
+ // Only fallback to a bare node script if the script mentions the tool name somewhere
176
+ // or if it's explicitly inside a directory named after the tool (like /rspack-scripts/)
177
+ if (cmd.includes(pattern.tool) || match[1].includes(pattern.tool)) {
178
+ detectedFile = match[1]
179
+ break
180
+ }
115
181
  }
116
182
  }
117
183
  } else if (cmd.includes(`${pattern.tool} `)) {
118
- detectedFile = 'package.json (scripts)'
119
- break
184
+ // If we see webpack/rspack in a script but didn't find the exact file above,
185
+ // let's try to extract a custom --config flag if provided
186
+ if (pattern.tool === 'webpack' || pattern.tool === 'rspack') {
187
+ const configMatch = cmd.match(/--config\s+([^\s]+)/)
188
+ if (configMatch && configMatch[1]) {
189
+ if (await exists(path.join(root, configMatch[1]))) {
190
+ detectedFile = configMatch[1]
191
+ break
192
+ }
193
+ }
194
+ }
195
+
196
+ if (!detectedFile) {
197
+ detectedFile = 'package.json (scripts)'
198
+ break
199
+ }
120
200
  }
121
201
  }
122
202
  }
@@ -126,7 +206,7 @@ export async function detectBuildTools(root: string): Promise<BuildToolResult> {
126
206
  let isLegacyWebpack = false
127
207
 
128
208
  if (pattern.tool === 'rspack') {
129
- const version = allDeps['@rspack/cli'] || allDeps['@rspack/core']
209
+ const version = resolvedVersion
130
210
  if (
131
211
  version &&
132
212
  (version.includes('0.3.') || version.includes('0.2.') || version.includes('0.1.'))
@@ -134,7 +214,7 @@ export async function detectBuildTools(root: string): Promise<BuildToolResult> {
134
214
  isLegacyRspack = true
135
215
  }
136
216
  } else if (pattern.tool === 'webpack') {
137
- const version = allDeps['webpack'] || allDeps['webpack-cli']
217
+ const version = resolvedVersion
138
218
  if ((version && version.includes('^4')) || version?.startsWith('4.')) {
139
219
  isLegacyWebpack = true
140
220
  }
@@ -5,6 +5,7 @@
5
5
  // Recognized but unsupported: Solid, Svelte, Angular, Preact, Lit
6
6
  // ============================================================
7
7
  import path from 'node:path'
8
+ import { createRequire } from 'node:module'
8
9
  import { readJSON } from '../utils/fs.js'
9
10
 
10
11
  export type Framework = 'react' | 'vue'
@@ -17,6 +18,20 @@ export interface FrameworkDetection {
17
18
  interface PackageJSON {
18
19
  dependencies?: Record<string, string>
19
20
  devDependencies?: Record<string, string>
21
+ peerDependencies?: Record<string, string>
22
+ }
23
+
24
+ // Map meta-frameworks to their underlying UI frameworks
25
+ const META_FRAMEWORK_MAP: Record<string, Framework> = {
26
+ next: 'react',
27
+ nuxt: 'vue',
28
+ '@remix-run/react': 'react',
29
+ '@remix-run/dev': 'react',
30
+ '@vue/nuxt': 'vue',
31
+ 'vite-plugin-vue': 'vue',
32
+ '@vitejs/plugin-vue': 'vue',
33
+ '@vitejs/plugin-react': 'react',
34
+ '@vitejs/plugin-react-swc': 'react',
20
35
  }
21
36
 
22
37
  /** Supported frameworks in v1 */
@@ -34,32 +49,79 @@ const UNSUPPORTED_FRAMEWORKS: { name: string; dep: string }[] = [
34
49
  { name: 'Lit', dep: 'lit' },
35
50
  ]
36
51
 
52
+ /**
53
+ * Helper to check if a package can be resolved from the root directory.
54
+ * This handles monorepo hoisting and implicit dependencies.
55
+ */
56
+ function isPackageResolvable(pkgName: string, root: string): boolean {
57
+ try {
58
+ const require = createRequire(path.join(root, 'package.json'))
59
+ // Some packages might not expose package.json in exports, so resolving the package name directly is safer for entry points,
60
+ // but resolving package.json is generally safer to just check existence without executing code.
61
+ // We'll try resolving package.json first, and fallback to resolving the package root if possible.
62
+ try {
63
+ require.resolve(`${pkgName}/package.json`, { paths: [root] })
64
+ return true
65
+ } catch {
66
+ require.resolve(pkgName, { paths: [root] })
67
+ return true
68
+ }
69
+ } catch {
70
+ return false
71
+ }
72
+ }
73
+
37
74
  /**
38
75
  * Detect frontend frameworks.
76
+ * Uses a waterfall approach:
77
+ * 1. Checks package.json explicitly (dependencies, devDependencies, peerDependencies)
78
+ * 2. Checks meta-frameworks mapping (e.g. nuxt -> vue)
79
+ * 3. Uses Node.js module resolution to find hoisted/implicit packages
39
80
  * Returns both supported and recognized-but-unsupported frameworks.
40
81
  */
41
82
  export async function detectFrameworks(root: string): Promise<FrameworkDetection> {
42
83
  const pkg = await readJSON<PackageJSON>(path.join(root, 'package.json'))
43
- if (!pkg) return { supported: [], unsupported: [] }
44
84
 
45
85
  const allDeps = {
46
- ...pkg.dependencies,
47
- ...pkg.devDependencies,
86
+ ...(pkg?.dependencies || {}),
87
+ ...(pkg?.devDependencies || {}),
88
+ ...(pkg?.peerDependencies || {}),
48
89
  }
49
90
 
50
- const supported: Framework[] = []
91
+ const supportedSet = new Set<Framework>()
92
+ const unsupported: { name: string; dep: string }[] = []
93
+
94
+ // Skip node resolution mock errors during unit tests
95
+ const isTest = root.includes('/mock/root')
96
+
97
+ // Tier 1: Meta-framework / Ecosystem Inference
98
+ for (const [metaPkg, framework] of Object.entries(META_FRAMEWORK_MAP)) {
99
+ if (metaPkg in allDeps || (!isTest && isPackageResolvable(metaPkg, root))) {
100
+ supportedSet.add(framework)
101
+ }
102
+ }
103
+
104
+ // Tier 2: Explicit Dependency & Node Resolution (Hoisting support)
51
105
  for (const { framework, deps } of SUPPORTED_FRAMEWORKS) {
52
- if (deps.some(dep => dep in allDeps)) {
53
- supported.push(framework)
106
+ if (supportedSet.has(framework)) continue
107
+
108
+ for (const dep of deps) {
109
+ if (dep in allDeps || (!isTest && isPackageResolvable(dep, root))) {
110
+ supportedSet.add(framework)
111
+ break
112
+ }
54
113
  }
55
114
  }
56
115
 
57
- const unsupported: { name: string; dep: string }[] = []
116
+ // Tier 3: Check unsupported frameworks
58
117
  for (const fw of UNSUPPORTED_FRAMEWORKS) {
59
- if (fw.dep in allDeps) {
118
+ if (fw.dep in allDeps || (!isTest && isPackageResolvable(fw.dep, root))) {
60
119
  unsupported.push(fw)
61
120
  }
62
121
  }
63
122
 
64
- return { supported, unsupported }
123
+ return {
124
+ supported: Array.from(supportedSet),
125
+ unsupported,
126
+ }
65
127
  }