@inspecto-dev/cli 0.2.0-alpha.2 → 0.2.0-alpha.4
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/.turbo/turbo-build.log +5 -5
- package/.turbo/turbo-test.log +21 -15
- package/CHANGELOG.md +22 -0
- package/dist/bin.js +1 -1
- package/dist/{chunk-V57BJXGZ.js → chunk-EUCQCD3Y.js} +309 -91
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/commands/init.ts +115 -19
- package/src/detect/build-tool.ts +227 -88
- package/src/detect/framework.ts +71 -9
- package/src/inject/extension.ts +6 -3
- package/src/types.ts +2 -0
- package/tests/build-tool.test.ts +46 -0
package/src/commands/init.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { detectProviders, type ProviderDetection } from '../detect/provider.js'
|
|
|
18
18
|
import { injectPlugin } from '../inject/ast-injector.js'
|
|
19
19
|
import { updateGitignore } from '../inject/gitignore.js'
|
|
20
20
|
import { installExtension } from '../inject/extension.js'
|
|
21
|
-
import type { InitOptions, InstallLock, Mutation } from '../types.js'
|
|
21
|
+
import type { InitOptions, InstallLock, Mutation, BuildToolDetection } from '../types.js'
|
|
22
22
|
import {
|
|
23
23
|
promptIDEChoice,
|
|
24
24
|
promptProviderChoice,
|
|
@@ -30,6 +30,29 @@ import { printNextJsManualInstructions, printNuxtManualInstructions } from '../i
|
|
|
30
30
|
export async function init(options: InitOptions): Promise<void> {
|
|
31
31
|
const root = process.cwd()
|
|
32
32
|
const mutations: Mutation[] = []
|
|
33
|
+
const normalizedPackages = normalizePackageList(options.packages)
|
|
34
|
+
|
|
35
|
+
const verifiedPackages: string[] = []
|
|
36
|
+
for (const pkg of normalizedPackages) {
|
|
37
|
+
if (!pkg) {
|
|
38
|
+
// Empty string represents the workspace root
|
|
39
|
+
verifiedPackages.push(pkg)
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const absolutePath = path.join(root, pkg)
|
|
44
|
+
if (await exists(absolutePath)) {
|
|
45
|
+
verifiedPackages.push(pkg)
|
|
46
|
+
} else {
|
|
47
|
+
log.warn(`Package path "${pkg}" not found (skipping)`)
|
|
48
|
+
log.hint('Ensure --packages values are relative to the project root')
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (normalizedPackages.length > 0 && verifiedPackages.length === 0) {
|
|
53
|
+
log.error('No valid packages found from --packages input')
|
|
54
|
+
return
|
|
55
|
+
}
|
|
33
56
|
|
|
34
57
|
log.header('Inspecto Setup')
|
|
35
58
|
|
|
@@ -44,7 +67,7 @@ export async function init(options: InitOptions): Promise<void> {
|
|
|
44
67
|
const [pm, frameworkResult, buildResult, ideProbe, providerProbe] = await Promise.all([
|
|
45
68
|
detectPackageManager(root),
|
|
46
69
|
detectFrameworks(root),
|
|
47
|
-
detectBuildTools(root),
|
|
70
|
+
detectBuildTools(root, verifiedPackages.length > 0 ? verifiedPackages : undefined),
|
|
48
71
|
detectIDE(root),
|
|
49
72
|
detectProviders(root),
|
|
50
73
|
])
|
|
@@ -57,12 +80,11 @@ export async function init(options: InitOptions): Promise<void> {
|
|
|
57
80
|
log.success(`Detected framework: ${frameworkResult.supported.join(', ')}`)
|
|
58
81
|
}
|
|
59
82
|
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
frameworkResult.supported.length === 0 && frameworkResult.unsupported.length === 0
|
|
83
|
+
const isSupported = frameworkResult.supported.length > 0
|
|
84
|
+
const hasUnsupported = frameworkResult.unsupported.length > 0
|
|
63
85
|
|
|
64
|
-
if (
|
|
65
|
-
if (
|
|
86
|
+
if (!isSupported) {
|
|
87
|
+
if (hasUnsupported) {
|
|
66
88
|
const names = frameworkResult.unsupported.map(f => f.name).join(', ')
|
|
67
89
|
log.warn(`Detected ${names} — not supported in v1 (React / Vue only)`)
|
|
68
90
|
} else {
|
|
@@ -79,10 +101,22 @@ export async function init(options: InitOptions): Promise<void> {
|
|
|
79
101
|
} else {
|
|
80
102
|
log.warn('Continuing anyway (--force)')
|
|
81
103
|
}
|
|
104
|
+
} else if (hasUnsupported) {
|
|
105
|
+
const names = frameworkResult.unsupported.map(f => f.name).join(', ')
|
|
106
|
+
log.hint(
|
|
107
|
+
`Note: Inspecto will be configured for ${frameworkResult.supported.join(', ')}. Other detected frameworks (${names}) will be ignored.`,
|
|
108
|
+
)
|
|
82
109
|
}
|
|
83
110
|
|
|
84
111
|
// Build tool detection
|
|
85
112
|
let manualConfigRequiredFor = ''
|
|
113
|
+
if (verifiedPackages.length > 0 && buildResult.supported.length === 0) {
|
|
114
|
+
log.warn(
|
|
115
|
+
`No supported build configs detected for: ${verifiedPackages.map(pkg => (pkg ? pkg : '.')).join(', ')}`,
|
|
116
|
+
)
|
|
117
|
+
log.hint('Double-check the --packages values or run without the flag to scan the repo root')
|
|
118
|
+
}
|
|
119
|
+
|
|
86
120
|
if (buildResult.supported.length > 0) {
|
|
87
121
|
buildResult.supported.forEach(bt => log.success(`Detected: ${bt.label}`))
|
|
88
122
|
}
|
|
@@ -97,7 +131,7 @@ export async function init(options: InitOptions): Promise<void> {
|
|
|
97
131
|
log.hint('current version supports: Vite, Webpack, Rspack, esbuild, Rollup')
|
|
98
132
|
log.hint('Dependency will be installed but plugin injection will be skipped')
|
|
99
133
|
log.hint(
|
|
100
|
-
'Please refer to the manual setup guide: https://inspecto.
|
|
134
|
+
'Please refer to the manual setup guide: https://inspecto-dev.github.io/inspecto/guide/manual-installation',
|
|
101
135
|
)
|
|
102
136
|
}
|
|
103
137
|
|
|
@@ -173,22 +207,54 @@ export async function init(options: InitOptions): Promise<void> {
|
|
|
173
207
|
// ---- Step 4: Inject plugin into build config ----
|
|
174
208
|
let injectionFailed = false
|
|
175
209
|
if (buildResult.supported.length > 0) {
|
|
176
|
-
|
|
210
|
+
if (verifiedPackages.length > 0) {
|
|
211
|
+
const targets = buildResult.supported.filter(detection =>
|
|
212
|
+
matchesAnyPackage(detection, verifiedPackages),
|
|
213
|
+
)
|
|
177
214
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
215
|
+
const unmatchedPackages = verifiedPackages.filter(
|
|
216
|
+
pkg => !buildResult.supported.some(detection => matchesPackage(detection, pkg)),
|
|
217
|
+
)
|
|
181
218
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
219
|
+
if (unmatchedPackages.length > 0) {
|
|
220
|
+
log.warn(
|
|
221
|
+
`No supported build configs detected for: ${unmatchedPackages
|
|
222
|
+
.map(pkg => (pkg ? pkg : '.'))
|
|
223
|
+
.join(', ')}`,
|
|
224
|
+
)
|
|
225
|
+
log.hint('Check the package paths or run without --packages to inspect the repo root')
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (targets.length === 0) {
|
|
187
229
|
injectionFailed = true
|
|
188
230
|
}
|
|
231
|
+
|
|
232
|
+
for (const target of targets) {
|
|
233
|
+
const result = await injectPlugin(root, target, options.dryRun)
|
|
234
|
+
if (result.success) {
|
|
235
|
+
mutations.push(...result.mutations)
|
|
236
|
+
} else {
|
|
237
|
+
injectionFailed = true
|
|
238
|
+
}
|
|
239
|
+
}
|
|
189
240
|
} else {
|
|
190
|
-
|
|
191
|
-
|
|
241
|
+
let target = resolveInjectionTarget(buildResult.supported)
|
|
242
|
+
|
|
243
|
+
if (target === 'ambiguous') {
|
|
244
|
+
target = await promptConfigChoice(buildResult.supported)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (target) {
|
|
248
|
+
const result = await injectPlugin(root, target, options.dryRun)
|
|
249
|
+
if (result.success) {
|
|
250
|
+
mutations.push(...result.mutations)
|
|
251
|
+
} else {
|
|
252
|
+
injectionFailed = true
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
injectionFailed = true
|
|
256
|
+
log.warn('Skipping plugin injection (manual configuration required)')
|
|
257
|
+
}
|
|
192
258
|
}
|
|
193
259
|
}
|
|
194
260
|
|
|
@@ -325,3 +391,33 @@ export async function init(options: InitOptions): Promise<void> {
|
|
|
325
391
|
log.ready('Ready! Hold Alt + Click any element to inspect.')
|
|
326
392
|
}
|
|
327
393
|
}
|
|
394
|
+
|
|
395
|
+
function normalizePackageList(packages?: string[]): string[] {
|
|
396
|
+
if (!packages || packages.length === 0) return []
|
|
397
|
+
|
|
398
|
+
const normalized = packages
|
|
399
|
+
.map(pkg => {
|
|
400
|
+
const trimmed = pkg.trim()
|
|
401
|
+
if (trimmed === '') return null
|
|
402
|
+
if (trimmed === '.' || trimmed === './') return ''
|
|
403
|
+
|
|
404
|
+
return trimmed.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/$/, '')
|
|
405
|
+
})
|
|
406
|
+
.filter((value): value is string => value !== null)
|
|
407
|
+
|
|
408
|
+
return Array.from(new Set(normalized))
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function matchesPackage(detection: BuildToolDetection, pkg: string): boolean {
|
|
412
|
+
const configPath = detection.configPath.replace(/\\/g, '/')
|
|
413
|
+
if (!pkg) {
|
|
414
|
+
return !configPath.includes('/')
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return configPath === pkg || configPath.startsWith(`${pkg}/`)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function matchesAnyPackage(detection: BuildToolDetection, packages: string[]): boolean {
|
|
421
|
+
if (packages.length === 0) return true
|
|
422
|
+
return packages.some(pkg => matchesPackage(detection, pkg))
|
|
423
|
+
}
|
package/src/detect/build-tool.ts
CHANGED
|
@@ -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,13 +13,54 @@ 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 */
|
|
18
53
|
const SUPPORTED_PATTERNS: { tool: BuildTool; files: string[]; label: string }[] = [
|
|
19
54
|
{
|
|
20
55
|
tool: 'vite',
|
|
21
|
-
files: [
|
|
56
|
+
files: [
|
|
57
|
+
'vite.config.ts',
|
|
58
|
+
'vite.config.js',
|
|
59
|
+
'vite.config.mts',
|
|
60
|
+
'vite.config.mjs',
|
|
61
|
+
'vite.config.cjs',
|
|
62
|
+
'vite.config.cts',
|
|
63
|
+
],
|
|
22
64
|
label: 'Vite',
|
|
23
65
|
},
|
|
24
66
|
{
|
|
@@ -57,126 +99,223 @@ const UNSUPPORTED_META: { name: string; dep: string; files: string[] }[] = [
|
|
|
57
99
|
{ name: 'SvelteKit', dep: '@sveltejs/kit', files: ['svelte.config.js', 'svelte.config.ts'] },
|
|
58
100
|
]
|
|
59
101
|
|
|
102
|
+
interface DetectionTarget {
|
|
103
|
+
/** Relative path provided via --packages ('' for repo root) */
|
|
104
|
+
packagePath: string
|
|
105
|
+
/** Absolute path to run detection from */
|
|
106
|
+
absolutePath: string
|
|
107
|
+
}
|
|
108
|
+
|
|
60
109
|
export interface BuildToolResult {
|
|
61
110
|
supported: BuildToolDetection[]
|
|
62
111
|
unsupported: string[]
|
|
63
112
|
}
|
|
64
113
|
|
|
114
|
+
function normalizeRelativePath(root: string, filePath: string): string {
|
|
115
|
+
const relative = path.relative(root, filePath)
|
|
116
|
+
const normalized = relative.split(path.sep).join('/')
|
|
117
|
+
return normalized || path.basename(filePath)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function createTargets(root: string, packagePaths?: string[]): DetectionTarget[] {
|
|
121
|
+
if (!packagePaths || packagePaths.length === 0) {
|
|
122
|
+
return [{ packagePath: '', absolutePath: root }]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return packagePaths.map(pkg => ({
|
|
126
|
+
packagePath: pkg,
|
|
127
|
+
absolutePath: pkg ? path.join(root, pkg) : root,
|
|
128
|
+
}))
|
|
129
|
+
}
|
|
130
|
+
|
|
65
131
|
/**
|
|
66
132
|
* Detect all build tools / meta-frameworks.
|
|
67
133
|
* Returns supported tools and recognized-but-unsupported meta-frameworks.
|
|
68
134
|
*/
|
|
69
|
-
export async function detectBuildTools(
|
|
135
|
+
export async function detectBuildTools(
|
|
136
|
+
root: string,
|
|
137
|
+
packagePaths?: string[],
|
|
138
|
+
): Promise<BuildToolResult> {
|
|
70
139
|
const supported: BuildToolDetection[] = []
|
|
71
|
-
const unsupported
|
|
72
|
-
|
|
73
|
-
// Detect supported build tools (by config file)
|
|
74
|
-
const pkg = await readJSON<PackageJSON>(path.join(root, 'package.json'))
|
|
75
|
-
const allDeps = { ...pkg?.dependencies, ...pkg?.devDependencies }
|
|
76
|
-
|
|
77
|
-
const supportedChecks = SUPPORTED_PATTERNS.map(async pattern => {
|
|
78
|
-
// 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]
|
|
87
|
-
|
|
88
|
-
// 2. Look for config files
|
|
89
|
-
let detectedFile = ''
|
|
90
|
-
|
|
91
|
-
// For esbuild, dependency is strictly required
|
|
92
|
-
if (pattern.tool === 'esbuild' && !hasDep) {
|
|
93
|
-
return null
|
|
94
|
-
}
|
|
140
|
+
const unsupported = new Set<string>()
|
|
141
|
+
const targets = createTargets(root, packagePaths)
|
|
95
142
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
}
|
|
143
|
+
for (const target of targets) {
|
|
144
|
+
const pkg = await readJSON<PackageJSON>(path.join(target.absolutePath, 'package.json'))
|
|
145
|
+
const allDeps = { ...pkg?.dependencies, ...pkg?.devDependencies }
|
|
146
|
+
const scripts = pkg?.scripts || {}
|
|
102
147
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
detectedFile = 'package.json (scripts)'
|
|
119
|
-
break
|
|
120
|
-
}
|
|
148
|
+
const supportedChecks = SUPPORTED_PATTERNS.map(pattern =>
|
|
149
|
+
detectPattern({
|
|
150
|
+
pattern,
|
|
151
|
+
workspaceRoot: root,
|
|
152
|
+
targetRoot: target.absolutePath,
|
|
153
|
+
packagePath: target.packagePath,
|
|
154
|
+
allDeps,
|
|
155
|
+
scripts,
|
|
156
|
+
}),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
const supportedResults = await Promise.all(supportedChecks)
|
|
160
|
+
for (const result of supportedResults) {
|
|
161
|
+
if (result) {
|
|
162
|
+
supported.push(result)
|
|
121
163
|
}
|
|
122
164
|
}
|
|
123
165
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const version = allDeps['@rspack/cli'] || allDeps['@rspack/core']
|
|
130
|
-
if (
|
|
131
|
-
version &&
|
|
132
|
-
(version.includes('0.3.') || version.includes('0.2.') || version.includes('0.1.'))
|
|
133
|
-
) {
|
|
134
|
-
isLegacyRspack = true
|
|
135
|
-
}
|
|
136
|
-
} else if (pattern.tool === 'webpack') {
|
|
137
|
-
const version = allDeps['webpack'] || allDeps['webpack-cli']
|
|
138
|
-
if ((version && version.includes('^4')) || version?.startsWith('4.')) {
|
|
139
|
-
isLegacyWebpack = true
|
|
166
|
+
const unsupportedChecks = UNSUPPORTED_META.map(async meta => {
|
|
167
|
+
if (!(meta.dep in allDeps)) return null
|
|
168
|
+
for (const file of meta.files) {
|
|
169
|
+
if (await exists(path.join(target.absolutePath, file))) {
|
|
170
|
+
return meta.name
|
|
140
171
|
}
|
|
141
172
|
}
|
|
173
|
+
return null
|
|
174
|
+
})
|
|
142
175
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
isLegacyRspack,
|
|
148
|
-
isLegacyWebpack,
|
|
176
|
+
const unsupportedResults = await Promise.all(unsupportedChecks)
|
|
177
|
+
for (const result of unsupportedResults) {
|
|
178
|
+
if (result) {
|
|
179
|
+
unsupported.add(result)
|
|
149
180
|
}
|
|
150
181
|
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { supported, unsupported: Array.from(unsupported) }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
interface PatternContext {
|
|
188
|
+
pattern: { tool: BuildTool; files: string[]; label: string }
|
|
189
|
+
workspaceRoot: string
|
|
190
|
+
targetRoot: string
|
|
191
|
+
packagePath: string
|
|
192
|
+
allDeps: Record<string, string | undefined>
|
|
193
|
+
scripts: Record<string, string>
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function detectPattern({
|
|
197
|
+
pattern,
|
|
198
|
+
workspaceRoot,
|
|
199
|
+
targetRoot,
|
|
200
|
+
packagePath,
|
|
201
|
+
allDeps,
|
|
202
|
+
scripts,
|
|
203
|
+
}: PatternContext): Promise<BuildToolDetection | null> {
|
|
204
|
+
let hasDep: boolean
|
|
205
|
+
let resolvedVersion: string | null = null
|
|
206
|
+
|
|
207
|
+
if (pattern.tool === 'rspack') {
|
|
208
|
+
const depName = allDeps['@rspack/cli'] ? '@rspack/cli' : '@rspack/core'
|
|
209
|
+
hasDep =
|
|
210
|
+
!!allDeps['@rspack/cli'] ||
|
|
211
|
+
!!allDeps['@rspack/core'] ||
|
|
212
|
+
isPackageResolvable('@rspack/core', targetRoot)
|
|
213
|
+
|
|
214
|
+
if (hasDep) {
|
|
215
|
+
resolvedVersion =
|
|
216
|
+
allDeps[depName] || (await getResolvedPackageVersion('@rspack/core', targetRoot))
|
|
217
|
+
}
|
|
218
|
+
} else if (pattern.tool === 'webpack') {
|
|
219
|
+
const depName = allDeps['webpack'] ? 'webpack' : 'webpack-cli'
|
|
220
|
+
hasDep =
|
|
221
|
+
!!allDeps['webpack'] || !!allDeps['webpack-cli'] || isPackageResolvable('webpack', targetRoot)
|
|
151
222
|
|
|
223
|
+
if (hasDep) {
|
|
224
|
+
resolvedVersion = allDeps[depName] || (await getResolvedPackageVersion('webpack', targetRoot))
|
|
225
|
+
}
|
|
226
|
+
} else if (pattern.tool === 'rsbuild') {
|
|
227
|
+
hasDep = !!allDeps['@rsbuild/core'] || isPackageResolvable('@rsbuild/core', targetRoot)
|
|
228
|
+
} else {
|
|
229
|
+
hasDep = !!allDeps[pattern.tool] || isPackageResolvable(pattern.tool, targetRoot)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let detectedFile = ''
|
|
233
|
+
|
|
234
|
+
if (pattern.tool === 'esbuild' && !hasDep) {
|
|
152
235
|
return null
|
|
153
|
-
}
|
|
236
|
+
}
|
|
154
237
|
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
238
|
+
for (const file of pattern.files) {
|
|
239
|
+
if (await exists(path.join(targetRoot, file))) {
|
|
240
|
+
detectedFile = file
|
|
241
|
+
break
|
|
159
242
|
}
|
|
160
243
|
}
|
|
161
244
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
245
|
+
if (
|
|
246
|
+
hasDep &&
|
|
247
|
+
!detectedFile &&
|
|
248
|
+
(pattern.tool === 'esbuild' ||
|
|
249
|
+
pattern.tool === 'rollup' ||
|
|
250
|
+
pattern.tool === 'webpack' ||
|
|
251
|
+
pattern.tool === 'rspack' ||
|
|
252
|
+
pattern.tool === 'rsbuild')
|
|
253
|
+
) {
|
|
254
|
+
for (const cmd of Object.values(scripts)) {
|
|
255
|
+
if (cmd.includes('node ')) {
|
|
256
|
+
const match = cmd.match(/node\s+([^\s]+\.(js|mjs|cjs|ts))/)
|
|
257
|
+
if (match && match[1]) {
|
|
258
|
+
if (await exists(path.join(targetRoot, match[1]))) {
|
|
259
|
+
if (cmd.includes(pattern.tool) || match[1].includes(pattern.tool)) {
|
|
260
|
+
detectedFile = match[1]
|
|
261
|
+
break
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} else if (cmd.includes(`${pattern.tool} `)) {
|
|
266
|
+
if (pattern.tool === 'webpack' || pattern.tool === 'rspack') {
|
|
267
|
+
const configMatch = cmd.match(/--config\s+([^\s]+)/)
|
|
268
|
+
if (configMatch && configMatch[1]) {
|
|
269
|
+
if (await exists(path.join(targetRoot, configMatch[1]))) {
|
|
270
|
+
detectedFile = configMatch[1]
|
|
271
|
+
break
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!detectedFile) {
|
|
277
|
+
detectedFile = 'package.json (scripts)'
|
|
278
|
+
break
|
|
279
|
+
}
|
|
167
280
|
}
|
|
168
281
|
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (!detectedFile) {
|
|
169
285
|
return null
|
|
170
|
-
}
|
|
286
|
+
}
|
|
171
287
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
288
|
+
let isLegacyRspack = false
|
|
289
|
+
let isLegacyWebpack = false
|
|
290
|
+
|
|
291
|
+
if (pattern.tool === 'rspack') {
|
|
292
|
+
const version = resolvedVersion
|
|
293
|
+
if (
|
|
294
|
+
version &&
|
|
295
|
+
(version.includes('0.3.') || version.includes('0.2.') || version.includes('0.1.'))
|
|
296
|
+
) {
|
|
297
|
+
isLegacyRspack = true
|
|
298
|
+
}
|
|
299
|
+
} else if (pattern.tool === 'webpack') {
|
|
300
|
+
const version = resolvedVersion
|
|
301
|
+
if ((version && version.includes('^4')) || version?.startsWith('4.')) {
|
|
302
|
+
isLegacyWebpack = true
|
|
176
303
|
}
|
|
177
304
|
}
|
|
178
305
|
|
|
179
|
-
|
|
306
|
+
const absoluteConfig = path.join(targetRoot, detectedFile)
|
|
307
|
+
const relativeConfig = normalizeRelativePath(workspaceRoot, absoluteConfig)
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
tool: pattern.tool,
|
|
311
|
+
configPath: relativeConfig,
|
|
312
|
+
label: `${pattern.label} (${relativeConfig})${isLegacyRspack ? ' [Legacy]' : ''}${
|
|
313
|
+
isLegacyWebpack ? ' [Webpack 4]' : ''
|
|
314
|
+
}`,
|
|
315
|
+
isLegacyRspack,
|
|
316
|
+
isLegacyWebpack,
|
|
317
|
+
packagePath: packagePath || undefined,
|
|
318
|
+
}
|
|
180
319
|
}
|
|
181
320
|
|
|
182
321
|
/**
|
package/src/detect/framework.ts
CHANGED
|
@@ -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
|
|
47
|
-
...pkg
|
|
86
|
+
...(pkg?.dependencies || {}),
|
|
87
|
+
...(pkg?.devDependencies || {}),
|
|
88
|
+
...(pkg?.peerDependencies || {}),
|
|
48
89
|
}
|
|
49
90
|
|
|
50
|
-
const
|
|
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 (
|
|
53
|
-
|
|
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
|
-
|
|
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 {
|
|
123
|
+
return {
|
|
124
|
+
supported: Array.from(supportedSet),
|
|
125
|
+
unsupported,
|
|
126
|
+
}
|
|
65
127
|
}
|