@inspecto-dev/cli 0.2.0-alpha.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/.turbo/turbo-build.log +19 -0
- package/.turbo/turbo-test.log +15 -0
- package/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/TESTING.md +109 -0
- package/bin/inspecto.js +3 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +83 -0
- package/dist/chunk-4RR7PTRN.js +1306 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +10 -0
- package/package.json +38 -0
- package/src/bin.ts +89 -0
- package/src/commands/doctor.ts +185 -0
- package/src/commands/init.ts +447 -0
- package/src/commands/teardown.ts +124 -0
- package/src/detect/ai-tool.ts +127 -0
- package/src/detect/build-tool.ts +123 -0
- package/src/detect/framework.ts +65 -0
- package/src/detect/ide.ts +78 -0
- package/src/detect/package-manager.ts +56 -0
- package/src/index.ts +5 -0
- package/src/inject/ast-injector.ts +300 -0
- package/src/inject/extension.ts +140 -0
- package/src/inject/gitignore.ts +76 -0
- package/src/types.ts +48 -0
- package/src/utils/exec.ts +44 -0
- package/src/utils/fs.ts +69 -0
- package/src/utils/logger.ts +64 -0
- package/tests/framework.test.ts +65 -0
- package/tests/ide.test.ts +94 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +10 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// src/detect/build-tool.ts — Build tool detection (v1)
|
|
3
|
+
//
|
|
4
|
+
// v1 supported: Vite / Webpack / Rspack / esbuild / Rollup
|
|
5
|
+
// Recognized but unsupported: Next.js / Nuxt / Remix / Astro / SvelteKit
|
|
6
|
+
// ============================================================
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
import { exists, readJSON } from '../utils/fs.js'
|
|
9
|
+
import type { BuildTool, BuildToolDetection } from '../types.js'
|
|
10
|
+
|
|
11
|
+
interface PackageJSON {
|
|
12
|
+
dependencies?: Record<string, string>
|
|
13
|
+
devDependencies?: Record<string, string>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Supported build tools in v1 */
|
|
17
|
+
const SUPPORTED_PATTERNS: { tool: BuildTool; files: string[]; label: string }[] = [
|
|
18
|
+
{
|
|
19
|
+
tool: 'vite',
|
|
20
|
+
files: ['vite.config.ts', 'vite.config.js', 'vite.config.mts', 'vite.config.mjs'],
|
|
21
|
+
label: 'Vite',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
tool: 'rspack',
|
|
25
|
+
files: ['rspack.config.js', 'rspack.config.ts', 'rspack.config.mjs'],
|
|
26
|
+
label: 'Rspack',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
tool: 'rsbuild',
|
|
30
|
+
files: ['rsbuild.config.js', 'rsbuild.config.ts', 'rsbuild.config.mjs'],
|
|
31
|
+
label: 'Rsbuild',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
tool: 'webpack',
|
|
35
|
+
files: ['webpack.config.js', 'webpack.config.ts', 'webpack.config.mjs', 'webpack.config.cjs'],
|
|
36
|
+
label: 'Webpack',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
tool: 'esbuild',
|
|
40
|
+
files: ['esbuild.config.js', 'esbuild.config.ts', 'esbuild.config.mjs'],
|
|
41
|
+
label: 'esbuild',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
tool: 'rollup',
|
|
45
|
+
files: ['rollup.config.js', 'rollup.config.ts', 'rollup.config.mjs'],
|
|
46
|
+
label: 'Rollup',
|
|
47
|
+
},
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
/** Recognized but unsupported meta-frameworks — detect via dep + config file */
|
|
51
|
+
const UNSUPPORTED_META: { name: string; dep: string; files: string[] }[] = [
|
|
52
|
+
{ name: 'Next.js', dep: 'next', files: ['next.config.mjs', 'next.config.js', 'next.config.ts'] },
|
|
53
|
+
{ name: 'Nuxt', dep: 'nuxt', files: ['nuxt.config.ts', 'nuxt.config.js'] },
|
|
54
|
+
{ name: 'Remix', dep: '@remix-run/dev', files: ['remix.config.js', 'remix.config.ts'] },
|
|
55
|
+
{ name: 'Astro', dep: 'astro', files: ['astro.config.mjs', 'astro.config.ts'] },
|
|
56
|
+
{ name: 'SvelteKit', dep: '@sveltejs/kit', files: ['svelte.config.js', 'svelte.config.ts'] },
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
export interface BuildToolResult {
|
|
60
|
+
supported: BuildToolDetection[]
|
|
61
|
+
unsupported: string[]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Detect all build tools / meta-frameworks.
|
|
66
|
+
* Returns supported tools and recognized-but-unsupported meta-frameworks.
|
|
67
|
+
*/
|
|
68
|
+
export async function detectBuildTools(root: string): Promise<BuildToolResult> {
|
|
69
|
+
const supported: BuildToolDetection[] = []
|
|
70
|
+
const unsupported: string[] = []
|
|
71
|
+
|
|
72
|
+
// Detect supported build tools (by config file)
|
|
73
|
+
const pkg = await readJSON<PackageJSON>(path.join(root, 'package.json'))
|
|
74
|
+
const allDeps = { ...pkg?.dependencies, ...pkg?.devDependencies }
|
|
75
|
+
|
|
76
|
+
for (const pattern of SUPPORTED_PATTERNS) {
|
|
77
|
+
for (const file of pattern.files) {
|
|
78
|
+
if (await exists(path.join(root, file))) {
|
|
79
|
+
let isLegacyRspack = false
|
|
80
|
+
if (pattern.tool === 'rspack') {
|
|
81
|
+
const version = allDeps['@rspack/cli'] || allDeps['@rspack/core']
|
|
82
|
+
if (
|
|
83
|
+
version &&
|
|
84
|
+
(version.includes('0.3.') || version.includes('0.2.') || version.includes('0.1.'))
|
|
85
|
+
) {
|
|
86
|
+
isLegacyRspack = true
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
supported.push({
|
|
91
|
+
tool: pattern.tool,
|
|
92
|
+
configPath: file,
|
|
93
|
+
label: `${pattern.label} (${file})${isLegacyRspack ? ' [Legacy]' : ''}`,
|
|
94
|
+
isLegacyRspack,
|
|
95
|
+
})
|
|
96
|
+
break // One match per tool
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const meta of UNSUPPORTED_META) {
|
|
102
|
+
if (!(meta.dep in allDeps)) continue
|
|
103
|
+
for (const file of meta.files) {
|
|
104
|
+
if (await exists(path.join(root, file))) {
|
|
105
|
+
unsupported.push(meta.name)
|
|
106
|
+
break
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { supported, unsupported }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Determine which detection to use for injection.
|
|
116
|
+
*/
|
|
117
|
+
export function resolveInjectionTarget(
|
|
118
|
+
detections: BuildToolDetection[],
|
|
119
|
+
): BuildToolDetection | null | 'ambiguous' {
|
|
120
|
+
if (detections.length === 0) return null
|
|
121
|
+
if (detections.length === 1) return detections[0]!
|
|
122
|
+
return 'ambiguous'
|
|
123
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// src/detect/framework.ts — Frontend framework detection
|
|
3
|
+
//
|
|
4
|
+
// v1 supported: React / Vue
|
|
5
|
+
// Recognized but unsupported: Solid, Svelte, Angular, Preact, Lit
|
|
6
|
+
// ============================================================
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
import { readJSON } from '../utils/fs.js'
|
|
9
|
+
|
|
10
|
+
export type Framework = 'react' | 'vue'
|
|
11
|
+
|
|
12
|
+
export interface FrameworkDetection {
|
|
13
|
+
supported: Framework[]
|
|
14
|
+
unsupported: { name: string; dep: string }[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface PackageJSON {
|
|
18
|
+
dependencies?: Record<string, string>
|
|
19
|
+
devDependencies?: Record<string, string>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Supported frameworks in v1 */
|
|
23
|
+
const SUPPORTED_FRAMEWORKS: { framework: Framework; deps: string[] }[] = [
|
|
24
|
+
{ framework: 'react', deps: ['react', 'react-dom'] },
|
|
25
|
+
{ framework: 'vue', deps: ['vue'] },
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
/** Recognized but not supported in v1 — detect and warn */
|
|
29
|
+
const UNSUPPORTED_FRAMEWORKS: { name: string; dep: string }[] = [
|
|
30
|
+
{ name: 'Solid', dep: 'solid-js' },
|
|
31
|
+
{ name: 'Svelte', dep: 'svelte' },
|
|
32
|
+
{ name: 'Angular', dep: '@angular/core' },
|
|
33
|
+
{ name: 'Preact', dep: 'preact' },
|
|
34
|
+
{ name: 'Lit', dep: 'lit' },
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Detect frontend frameworks.
|
|
39
|
+
* Returns both supported and recognized-but-unsupported frameworks.
|
|
40
|
+
*/
|
|
41
|
+
export async function detectFrameworks(root: string): Promise<FrameworkDetection> {
|
|
42
|
+
const pkg = await readJSON<PackageJSON>(path.join(root, 'package.json'))
|
|
43
|
+
if (!pkg) return { supported: [], unsupported: [] }
|
|
44
|
+
|
|
45
|
+
const allDeps = {
|
|
46
|
+
...pkg.dependencies,
|
|
47
|
+
...pkg.devDependencies,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const supported: Framework[] = []
|
|
51
|
+
for (const { framework, deps } of SUPPORTED_FRAMEWORKS) {
|
|
52
|
+
if (deps.some(dep => dep in allDeps)) {
|
|
53
|
+
supported.push(framework)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const unsupported: { name: string; dep: string }[] = []
|
|
58
|
+
for (const fw of UNSUPPORTED_FRAMEWORKS) {
|
|
59
|
+
if (fw.dep in allDeps) {
|
|
60
|
+
unsupported.push(fw)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { supported, unsupported }
|
|
65
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// src/detect/ide.ts — IDE detection (v1: VS Code only)
|
|
3
|
+
//
|
|
4
|
+
// Detects ALL known IDEs, but only VS Code is fully supported.
|
|
5
|
+
// Others are recognized and surfaced as unsupported.
|
|
6
|
+
// ============================================================
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
import { exists } from '../utils/fs.js'
|
|
9
|
+
|
|
10
|
+
export type IDEDetection = {
|
|
11
|
+
ide: string
|
|
12
|
+
supported: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type IDEProbeResult = {
|
|
16
|
+
detected: IDEDetection[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const SUPPORTED_IDE = 'vscode'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Detect all installed IDEs based on directory artifacts and environment.
|
|
23
|
+
*/
|
|
24
|
+
export async function detectIDE(root: string): Promise<IDEProbeResult> {
|
|
25
|
+
const detected: Map<string, IDEDetection> = new Map()
|
|
26
|
+
|
|
27
|
+
// 1. Check Terminal Environment (Highest confidence for current session)
|
|
28
|
+
|
|
29
|
+
// Cursor
|
|
30
|
+
if (process.env.CURSOR_TRACE_DIR || process.env.CURSOR_CHANNEL) {
|
|
31
|
+
detected.set('Cursor', { ide: 'Cursor', supported: false })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Trae
|
|
35
|
+
if (
|
|
36
|
+
process.env.TRAE_APP_DIR ||
|
|
37
|
+
process.env.__CFBundleIdentifier === 'com.byteocean.trae' ||
|
|
38
|
+
process.env.COCO_IDE_PLUGIN_TYPE === 'Trae' ||
|
|
39
|
+
(process.env.npm_config_user_agent && process.env.npm_config_user_agent.includes('trae'))
|
|
40
|
+
) {
|
|
41
|
+
detected.set('Trae', { ide: 'Trae', supported: false })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Zed
|
|
45
|
+
if (process.env.ZED_TERM) {
|
|
46
|
+
detected.set('Zed', { ide: 'Zed', supported: false })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// VS Code
|
|
50
|
+
// Must ensure we haven't already flagged it as Trae/Cursor since they share this variable
|
|
51
|
+
if (process.env.TERM_PROGRAM === 'vscode') {
|
|
52
|
+
if (!detected.has('Trae') && !detected.has('Cursor')) {
|
|
53
|
+
detected.set('vscode', { ide: SUPPORTED_IDE, supported: true })
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 2. Check Directory Artifacts (Indicates project has been opened in these IDEs)
|
|
58
|
+
if (await exists(path.join(root, '.trae'))) {
|
|
59
|
+
detected.set('Trae', { ide: 'Trae', supported: false })
|
|
60
|
+
}
|
|
61
|
+
if (await exists(path.join(root, '.cursor'))) {
|
|
62
|
+
detected.set('Cursor', { ide: 'Cursor', supported: false })
|
|
63
|
+
}
|
|
64
|
+
if (await exists(path.join(root, '.vscode'))) {
|
|
65
|
+
// Only add vscode if it wasn't already caught by terminal,
|
|
66
|
+
// or if the terminal is something else but .vscode exists.
|
|
67
|
+
if (!detected.has('vscode')) {
|
|
68
|
+
detected.set('vscode', { ide: SUPPORTED_IDE, supported: true })
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (await exists(path.join(root, '.idea'))) {
|
|
72
|
+
detected.set('JetBrains IDE', { ide: 'JetBrains IDE', supported: false })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
detected: Array.from(detected.values()),
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// src/detect/package-manager.ts — Lockfile-based PM detection
|
|
3
|
+
// ============================================================
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { exists } from '../utils/fs.js'
|
|
6
|
+
import type { PackageManager } from '../types.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Detect the package manager by checking lockfile presence.
|
|
10
|
+
* Priority: bun > pnpm > yarn > npm (fallback)
|
|
11
|
+
*/
|
|
12
|
+
export async function detectPackageManager(root: string): Promise<PackageManager> {
|
|
13
|
+
const checks: [string, PackageManager][] = [
|
|
14
|
+
['bun.lockb', 'bun'],
|
|
15
|
+
['bun.lock', 'bun'],
|
|
16
|
+
['pnpm-lock.yaml', 'pnpm'],
|
|
17
|
+
['yarn.lock', 'yarn'],
|
|
18
|
+
['package-lock.json', 'npm'],
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
for (const [file, pm] of checks) {
|
|
22
|
+
if (await exists(path.join(root, file))) {
|
|
23
|
+
return pm
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return 'npm' // fallback
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Get the install command for a package manager */
|
|
31
|
+
export function getInstallCommand(pm: PackageManager, pkg: string): string {
|
|
32
|
+
switch (pm) {
|
|
33
|
+
case 'bun':
|
|
34
|
+
return `bun add -D ${pkg}`
|
|
35
|
+
case 'pnpm':
|
|
36
|
+
return `pnpm add -D ${pkg}`
|
|
37
|
+
case 'yarn':
|
|
38
|
+
return `yarn add -D ${pkg}`
|
|
39
|
+
case 'npm':
|
|
40
|
+
return `npm install -D ${pkg}`
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Get the uninstall command for a package manager */
|
|
45
|
+
export function getUninstallCommand(pm: PackageManager, pkg: string): string {
|
|
46
|
+
switch (pm) {
|
|
47
|
+
case 'bun':
|
|
48
|
+
return `bun remove ${pkg}`
|
|
49
|
+
case 'pnpm':
|
|
50
|
+
return `pnpm remove ${pkg}`
|
|
51
|
+
case 'yarn':
|
|
52
|
+
return `yarn remove ${pkg}`
|
|
53
|
+
case 'npm':
|
|
54
|
+
return `npm uninstall ${pkg}`
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { init } from './commands/init.js'
|
|
2
|
+
export { doctor } from './commands/doctor.js'
|
|
3
|
+
export { teardown } from './commands/teardown.js'
|
|
4
|
+
export type { InitOptions, BuildTool, PackageManager, InstallLock } from './types.js'
|
|
5
|
+
export type { Framework } from './detect/framework.js'
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// src/inject/ast-injector.ts — Safe plugin injection (v1)
|
|
3
|
+
//
|
|
4
|
+
// v1 scope: Standard `plugins: [...]` array injection.
|
|
5
|
+
// Covers Vite / Webpack / Rspack / esbuild / Rollup configs.
|
|
6
|
+
// Meta-framework special strategies (Next.js, Nuxt) deferred.
|
|
7
|
+
//
|
|
8
|
+
// Safety protocol:
|
|
9
|
+
// 1. Backup original file (.bak)
|
|
10
|
+
// 2. Idempotency check (skip if already injected)
|
|
11
|
+
// 3. Attempt regex-based injection
|
|
12
|
+
// 4. Basic syntax validation
|
|
13
|
+
// 5. Graceful degradation on any failure
|
|
14
|
+
// ============================================================
|
|
15
|
+
|
|
16
|
+
import path from 'node:path'
|
|
17
|
+
import { exists, readFile, writeFile, copyFile } from '../utils/fs.js'
|
|
18
|
+
import { log } from '../utils/logger.js'
|
|
19
|
+
import type { BuildTool, BuildToolDetection, Mutation } from '../types.js'
|
|
20
|
+
|
|
21
|
+
// ---- Import & plugin expression per build tool ----
|
|
22
|
+
|
|
23
|
+
const IMPORT_MAP: Record<BuildTool, string> = {
|
|
24
|
+
vite: `import { vitePlugin as inspecto } from '@inspecto-dev/plugin'`,
|
|
25
|
+
webpack: `import { webpackPlugin as inspecto } from '@inspecto-dev/plugin'`,
|
|
26
|
+
rspack: `import { rspackPlugin as inspecto } from '@inspecto-dev/plugin'`,
|
|
27
|
+
rsbuild: `import { rspackPlugin as inspecto } from '@inspecto-dev/plugin'`,
|
|
28
|
+
esbuild: `import { esbuildPlugin as inspecto } from '@inspecto-dev/plugin'`,
|
|
29
|
+
rollup: `import { rollupPlugin as inspecto } from '@inspecto-dev/plugin'`,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getImportStatement(tool: BuildTool, isLegacyRspack?: boolean): string {
|
|
33
|
+
if (tool === 'rspack' && isLegacyRspack) {
|
|
34
|
+
return `import { rspackPlugin as inspecto } from '@inspecto-dev/plugin/legacy/rspack'`
|
|
35
|
+
}
|
|
36
|
+
return IMPORT_MAP[tool]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getPluginExpression(isLegacyRspack?: boolean): string {
|
|
40
|
+
if (isLegacyRspack) {
|
|
41
|
+
return `process.env.NODE_ENV !== 'production' && inspecto({
|
|
42
|
+
pathType: 'absolute',
|
|
43
|
+
escapeTags: ['Transition', 'AnimatePresence'],
|
|
44
|
+
}) as any`
|
|
45
|
+
}
|
|
46
|
+
return `process.env.NODE_ENV !== 'production' && inspecto()`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---- Manual fallback instructions ----
|
|
50
|
+
|
|
51
|
+
function printManualInstructions(
|
|
52
|
+
tool: BuildTool,
|
|
53
|
+
configPath: string,
|
|
54
|
+
reason: string,
|
|
55
|
+
isLegacyRspack?: boolean,
|
|
56
|
+
) {
|
|
57
|
+
const isRsbuild = tool === 'rsbuild'
|
|
58
|
+
|
|
59
|
+
log.warn(`Could not safely auto-inject into ${configPath}`)
|
|
60
|
+
log.hint(`(reason: ${reason})`)
|
|
61
|
+
log.blank()
|
|
62
|
+
log.hint('Please add the following manually:')
|
|
63
|
+
|
|
64
|
+
if (isRsbuild) {
|
|
65
|
+
log.codeBlock([
|
|
66
|
+
getImportStatement(tool, isLegacyRspack),
|
|
67
|
+
'',
|
|
68
|
+
'// Add to tools.rspack:',
|
|
69
|
+
`tools: {`,
|
|
70
|
+
` rspack: {`,
|
|
71
|
+
` plugins: [`,
|
|
72
|
+
` ${getPluginExpression(isLegacyRspack)},`,
|
|
73
|
+
` ]`,
|
|
74
|
+
` }`,
|
|
75
|
+
`}`,
|
|
76
|
+
])
|
|
77
|
+
} else {
|
|
78
|
+
log.codeBlock([
|
|
79
|
+
getImportStatement(tool, isLegacyRspack),
|
|
80
|
+
'',
|
|
81
|
+
'// Add to your plugins array:',
|
|
82
|
+
`plugins: [`,
|
|
83
|
+
` ${getPluginExpression(isLegacyRspack)},`,
|
|
84
|
+
` ...otherPlugins`,
|
|
85
|
+
`].filter(Boolean)`,
|
|
86
|
+
])
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---- Injection helpers ----
|
|
91
|
+
|
|
92
|
+
/** Check if inspecto is already injected (idempotency). */
|
|
93
|
+
function isAlreadyInjected(content: string): boolean {
|
|
94
|
+
return (
|
|
95
|
+
content.includes('@inspecto-dev/plugin') ||
|
|
96
|
+
content.includes('inspecto()') ||
|
|
97
|
+
content.includes('aiDevInspector')
|
|
98
|
+
) // Legacy support
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Inject import statement after the last existing import. */
|
|
102
|
+
function injectImport(content: string, importStmt: string): string {
|
|
103
|
+
const importRegex = /^import\s.+$/gm
|
|
104
|
+
let lastImportEnd = 0
|
|
105
|
+
let match: RegExpExecArray | null
|
|
106
|
+
|
|
107
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
108
|
+
const lineEnd = content.indexOf('\n', match.index)
|
|
109
|
+
if (lineEnd > lastImportEnd) {
|
|
110
|
+
lastImportEnd = lineEnd
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Also handle CJS require patterns
|
|
115
|
+
if (lastImportEnd === 0) {
|
|
116
|
+
const requireRegex = /^(?:const|let|var)\s.+=\s*require\(.+\).*$/gm
|
|
117
|
+
while ((match = requireRegex.exec(content)) !== null) {
|
|
118
|
+
const lineEnd = content.indexOf('\n', match.index)
|
|
119
|
+
if (lineEnd > lastImportEnd) {
|
|
120
|
+
lastImportEnd = lineEnd
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (lastImportEnd > 0) {
|
|
126
|
+
return content.slice(0, lastImportEnd) + '\n' + importStmt + content.slice(lastImportEnd)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// No imports found, add at the beginning
|
|
130
|
+
return importStmt + '\n\n' + content
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Core injection strategy.
|
|
135
|
+
* For most tools: find `plugins: [` and insert after `[`.
|
|
136
|
+
* For rsbuild: find `tools: { rspack: { plugins: [` or try to add it.
|
|
137
|
+
*/
|
|
138
|
+
function injectIntoPluginsArray(content: string, detection: BuildToolDetection): string | null {
|
|
139
|
+
const tool = detection.tool
|
|
140
|
+
if (tool === 'rsbuild') {
|
|
141
|
+
// rsbuild needs to inject into tools.rspack.plugins or tools.rspack(config, { appendPlugins })
|
|
142
|
+
// Due to the complexity of rsbuild config, we'll try a simple regex for `tools: { rspack: { plugins: [`
|
|
143
|
+
// If that fails, we fallback to manual instructions which are explicitly for rsbuild.
|
|
144
|
+
|
|
145
|
+
// Check if tools.rspack exists
|
|
146
|
+
if (!content.includes('tools:') && !content.includes('rspack:')) {
|
|
147
|
+
// Very basic config, we can try to inject before the closing brace of defineConfig
|
|
148
|
+
const exportRegex = /(export default defineConfig\(\{)/
|
|
149
|
+
const match = exportRegex.exec(content)
|
|
150
|
+
if (match) {
|
|
151
|
+
const insertPos = match.index + match[0].length
|
|
152
|
+
const pluginExpr = `\n tools: {\n rspack: {\n plugins: [\n ${getPluginExpression(detection.isLegacyRspack)}\n ]\n }\n },`
|
|
153
|
+
return content.slice(0, insertPos) + pluginExpr + content.slice(insertPos)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Otherwise it's too complex for simple regex, force manual degradation
|
|
158
|
+
return null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const pluginsRegex = /(plugins\s*:\s*\[)/
|
|
162
|
+
const match = pluginsRegex.exec(content)
|
|
163
|
+
if (!match) return null
|
|
164
|
+
|
|
165
|
+
const insertPos = match.index + match[0].length
|
|
166
|
+
const pluginExpr = `\n ${getPluginExpression(detection.isLegacyRspack)},`
|
|
167
|
+
|
|
168
|
+
return content.slice(0, insertPos) + pluginExpr + content.slice(insertPos)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Basic bracket-balance validation. */
|
|
172
|
+
function validateBrackets(content: string): boolean {
|
|
173
|
+
const openBraces = (content.match(/\{/g) || []).length
|
|
174
|
+
const closeBraces = (content.match(/\}/g) || []).length
|
|
175
|
+
const openBrackets = (content.match(/\[/g) || []).length
|
|
176
|
+
const closeBrackets = (content.match(/\]/g) || []).length
|
|
177
|
+
|
|
178
|
+
return Math.abs(openBraces - closeBraces) <= 1 && Math.abs(openBrackets - closeBrackets) <= 1
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---- Main injection orchestrator ----
|
|
182
|
+
|
|
183
|
+
export interface InjectionResult {
|
|
184
|
+
success: boolean
|
|
185
|
+
mutations: Mutation[]
|
|
186
|
+
failureReason?: string
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function injectPlugin(
|
|
190
|
+
root: string,
|
|
191
|
+
detection: BuildToolDetection,
|
|
192
|
+
dryRun: boolean,
|
|
193
|
+
): Promise<InjectionResult> {
|
|
194
|
+
const configPath = path.join(root, detection.configPath)
|
|
195
|
+
const backupPath = configPath + '.bak'
|
|
196
|
+
const mutations: Mutation[] = []
|
|
197
|
+
|
|
198
|
+
// Step 1: Read config file
|
|
199
|
+
const content = await readFile(configPath)
|
|
200
|
+
if (!content) {
|
|
201
|
+
printManualInstructions(
|
|
202
|
+
detection.tool,
|
|
203
|
+
detection.configPath,
|
|
204
|
+
'config file not readable',
|
|
205
|
+
detection.isLegacyRspack,
|
|
206
|
+
)
|
|
207
|
+
return { success: false, mutations, failureReason: 'config file not readable' }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Step 2: Idempotency check
|
|
211
|
+
if (isAlreadyInjected(content)) {
|
|
212
|
+
log.success(`Plugin already injected in ${detection.configPath} (skipped)`)
|
|
213
|
+
|
|
214
|
+
// We still want to record the mutation so teardown knows how to clean it up
|
|
215
|
+
// However, if the user manually added it, we shouldn't overwrite their code with a .bak on teardown.
|
|
216
|
+
// To handle this, we register a special 'plugin_injected' mutation instead of 'file_modified',
|
|
217
|
+
// which tells teardown to use AST-removal instead of .bak restoration (if we implement it)
|
|
218
|
+
// For now, we will assume if it's already there and a .bak exists, we track it for restoration.
|
|
219
|
+
if (await exists(backupPath)) {
|
|
220
|
+
mutations.push({
|
|
221
|
+
type: 'file_modified',
|
|
222
|
+
path: detection.configPath,
|
|
223
|
+
backup: detection.configPath + '.bak',
|
|
224
|
+
description: 'Previously injected inspecto() plugin',
|
|
225
|
+
})
|
|
226
|
+
} else {
|
|
227
|
+
// If no backup exists, we just mark it as modified but without backup
|
|
228
|
+
// This tells teardown it was touched but cannot be safely rolled back
|
|
229
|
+
mutations.push({
|
|
230
|
+
type: 'file_modified',
|
|
231
|
+
path: detection.configPath,
|
|
232
|
+
description: 'Previously injected inspecto() plugin (no backup)',
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { success: true, mutations }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Step 3: Backup
|
|
240
|
+
if (!dryRun) {
|
|
241
|
+
await copyFile(configPath, backupPath)
|
|
242
|
+
mutations.push({
|
|
243
|
+
type: 'file_modified',
|
|
244
|
+
path: detection.configPath,
|
|
245
|
+
backup: detection.configPath + '.bak',
|
|
246
|
+
description: 'Injected inspecto() plugin',
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
log.success(`Backed up ${detection.configPath} → ${detection.configPath}.bak`)
|
|
250
|
+
|
|
251
|
+
// Step 4: Inject into plugins array
|
|
252
|
+
const injected = injectIntoPluginsArray(content, detection)
|
|
253
|
+
if (!injected) {
|
|
254
|
+
printManualInstructions(
|
|
255
|
+
detection.tool,
|
|
256
|
+
detection.configPath,
|
|
257
|
+
'could not locate plugins array — file may use dynamic config, function wrapper, or non-standard export',
|
|
258
|
+
detection.isLegacyRspack,
|
|
259
|
+
)
|
|
260
|
+
return {
|
|
261
|
+
success: false,
|
|
262
|
+
mutations,
|
|
263
|
+
failureReason: 'could not locate plugins array',
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Step 5: Inject import statement
|
|
268
|
+
const importStmt = getImportStatement(detection.tool, detection.isLegacyRspack)
|
|
269
|
+
const modifiedContent = injectImport(injected, importStmt)
|
|
270
|
+
|
|
271
|
+
// Step 6: Bracket-balance validation
|
|
272
|
+
if (!validateBrackets(modifiedContent)) {
|
|
273
|
+
log.error('Syntax validation failed after injection')
|
|
274
|
+
if (!dryRun) {
|
|
275
|
+
await copyFile(backupPath, configPath)
|
|
276
|
+
log.success(`Restored ${detection.configPath} from backup`)
|
|
277
|
+
}
|
|
278
|
+
printManualInstructions(
|
|
279
|
+
detection.tool,
|
|
280
|
+
detection.configPath,
|
|
281
|
+
'injection produced unbalanced brackets',
|
|
282
|
+
detection.isLegacyRspack,
|
|
283
|
+
)
|
|
284
|
+
return {
|
|
285
|
+
success: false,
|
|
286
|
+
mutations: [], // Rolled back
|
|
287
|
+
failureReason: 'injection produced invalid syntax',
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Step 7: Write
|
|
292
|
+
if (dryRun) {
|
|
293
|
+
log.dryRun(`Would inject plugin into ${detection.configPath}`)
|
|
294
|
+
} else {
|
|
295
|
+
await writeFile(configPath, modifiedContent)
|
|
296
|
+
log.success(`Injected plugin into ${detection.configPath}`)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return { success: true, mutations }
|
|
300
|
+
}
|