legacy-modernizer 0.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.
- package/README.md +290 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +534 -0
- package/dist/index.mjs +497 -0
- package/dist/reporters/analysis.d.ts +8 -0
- package/dist/reporters/analysis.test.d.ts +1 -0
- package/dist/reporters/locale.d.ts +2 -0
- package/dist/scanners/legacy-scanner.d.ts +16 -0
- package/dist/scanners/legacy-scanner.test.d.ts +1 -0
- package/dist/scanners/rules/eslint-modernize.d.ts +3 -0
- package/dist/scanners/rules/index.d.ts +3 -0
- package/dist/scanners/rules/jest-to-vitest.d.ts +2 -0
- package/dist/scanners/rules/js-to-ts.d.ts +3 -0
- package/dist/scanners/rules/vue2-to-vue3.d.ts +2 -0
- package/dist/scanners/rules/vuex-to-pinia.d.ts +2 -0
- package/dist/scanners/rules/webpack-to-vite.d.ts +2 -0
- package/dist/types.d.ts +140 -0
- package/dist/utils/config.d.ts +8 -0
- package/dist/utils/format.d.ts +13 -0
- package/package.json +73 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
// src/utils/format.ts
|
|
2
|
+
var SEVERITY_BADGE = {
|
|
3
|
+
critical: "\u{1F534}",
|
|
4
|
+
warning: "\u{1F7E1}",
|
|
5
|
+
info: "\u{1F535}"
|
|
6
|
+
};
|
|
7
|
+
var DIMENSION_LABELS = {
|
|
8
|
+
"vue2-to-vue3": { en: "Vue 2 \u2192 3", zh: "Vue 2 \u2192 3" },
|
|
9
|
+
"js-to-ts": { en: "JS \u2192 TS", zh: "JS \u2192 TS" },
|
|
10
|
+
"webpack-to-vite": { en: "Webpack \u2192 Vite", zh: "Webpack \u2192 Vite" },
|
|
11
|
+
"jest-to-vitest": { en: "Jest \u2192 Vitest", zh: "Jest \u2192 Vitest" },
|
|
12
|
+
"vuex-to-pinia": { en: "Vuex \u2192 Pinia", zh: "Vuex \u2192 Pinia" },
|
|
13
|
+
"eslint-modernize": { en: "ESLint Modernize", zh: "ESLint \u73B0\u4EE3\u5316" }
|
|
14
|
+
};
|
|
15
|
+
function patternToMarkdown(p) {
|
|
16
|
+
const badge = SEVERITY_BADGE[p.severity] ?? "\u2753";
|
|
17
|
+
const dim = DIMENSION_LABELS[p.dimension]?.zh ?? p.dimension;
|
|
18
|
+
const loc = p.line > 0 ? `:${p.line}` : "";
|
|
19
|
+
const line = `- ${badge} **${p.name}** [${dim}] \`${p.file}${loc}\``;
|
|
20
|
+
return p.suggestion ? `${line}
|
|
21
|
+
\u2192 ${p.suggestion}` : line;
|
|
22
|
+
}
|
|
23
|
+
function severityBadge(severity) {
|
|
24
|
+
return SEVERITY_BADGE[severity] ?? "\u2753";
|
|
25
|
+
}
|
|
26
|
+
function dimensionLabel(dimension, locale = "zh") {
|
|
27
|
+
return DIMENSION_LABELS[dimension]?.[locale] ?? dimension;
|
|
28
|
+
}
|
|
29
|
+
function formatDuration(ms) {
|
|
30
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
31
|
+
const seconds = Math.round(ms / 1e3);
|
|
32
|
+
if (seconds < 60) return `${seconds}s`;
|
|
33
|
+
const minutes = Math.floor(seconds / 60);
|
|
34
|
+
const secs = seconds % 60;
|
|
35
|
+
return secs > 0 ? `${minutes}m ${secs}s` : `${minutes}m`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/reporters/locale.ts
|
|
39
|
+
var LABELS = {
|
|
40
|
+
en: {
|
|
41
|
+
title: "# \u{1F50D} Legacy Modernizer Analysis Report",
|
|
42
|
+
overview: "## Overview",
|
|
43
|
+
projectRoot: "Project Root",
|
|
44
|
+
scannedAt: "Scanned At",
|
|
45
|
+
totalFiles: "Files Scanned",
|
|
46
|
+
totalPatterns: "Patterns Found",
|
|
47
|
+
duration: "Duration",
|
|
48
|
+
dimensions: "## Dimensions",
|
|
49
|
+
dimension: "Dimension",
|
|
50
|
+
count: "Count",
|
|
51
|
+
affectedFiles: "Files",
|
|
52
|
+
critical: "Critical",
|
|
53
|
+
warning: "Warning",
|
|
54
|
+
info: "Info",
|
|
55
|
+
topPatterns: "## Top Legacy Patterns",
|
|
56
|
+
riskAssessment: "## Risk Assessment",
|
|
57
|
+
riskLevel: "Risk Level",
|
|
58
|
+
reason: "Reason",
|
|
59
|
+
recommendedOrder: "Recommended Order",
|
|
60
|
+
estimatedEffort: "Estimated Effort",
|
|
61
|
+
personDays: "person-days",
|
|
62
|
+
suggestions: "## \u{1F4A1} Suggestions",
|
|
63
|
+
suggestion1: "Prioritize critical-severity legacy patterns",
|
|
64
|
+
suggestion2: "Follow the recommended migration order",
|
|
65
|
+
suggestion3: "Run tests after completing each dimension",
|
|
66
|
+
suggestion4: "Use `/modernize` to launch the interactive migration wizard"
|
|
67
|
+
},
|
|
68
|
+
zh: {
|
|
69
|
+
title: "# \u{1F50D} Legacy Modernizer \u5206\u6790\u62A5\u544A",
|
|
70
|
+
overview: "## \u9879\u76EE\u6982\u89C8",
|
|
71
|
+
projectRoot: "\u9879\u76EE\u8DEF\u5F84",
|
|
72
|
+
scannedAt: "\u626B\u63CF\u65F6\u95F4",
|
|
73
|
+
totalFiles: "\u626B\u63CF\u6587\u4EF6",
|
|
74
|
+
totalPatterns: "\u53D1\u73B0\u6A21\u5F0F",
|
|
75
|
+
duration: "\u626B\u63CF\u8017\u65F6",
|
|
76
|
+
dimensions: "## \u7EF4\u5EA6\u7EDF\u8BA1",
|
|
77
|
+
dimension: "\u7EF4\u5EA6",
|
|
78
|
+
count: "\u53D1\u73B0\u6570",
|
|
79
|
+
affectedFiles: "\u53D7\u5F71\u54CD\u6587\u4EF6",
|
|
80
|
+
critical: "\u4E25\u91CD",
|
|
81
|
+
warning: "\u8B66\u544A",
|
|
82
|
+
info: "\u5EFA\u8BAE",
|
|
83
|
+
topPatterns: "## Top \u9057\u7559\u6A21\u5F0F",
|
|
84
|
+
riskAssessment: "## \u98CE\u9669\u8BC4\u4F30",
|
|
85
|
+
riskLevel: "\u6574\u4F53\u98CE\u9669",
|
|
86
|
+
reason: "\u539F\u56E0",
|
|
87
|
+
recommendedOrder: "\u63A8\u8350\u8FC1\u79FB\u987A\u5E8F",
|
|
88
|
+
estimatedEffort: "\u9884\u4F30\u5DE5\u65F6",
|
|
89
|
+
personDays: "\u4EBA\u5929",
|
|
90
|
+
suggestions: "## \u{1F4A1} \u5EFA\u8BAE",
|
|
91
|
+
suggestion1: "\u4F18\u5148\u5904\u7406\u4E25\u91CD\u7EA7\u522B\u7684\u9057\u7559\u6A21\u5F0F",
|
|
92
|
+
suggestion2: "\u6309\u63A8\u8350\u8FC1\u79FB\u987A\u5E8F\u9010\u6B65\u63A8\u8FDB",
|
|
93
|
+
suggestion3: "\u6BCF\u4E2A\u7EF4\u5EA6\u5B8C\u6210\u540E\u8FD0\u884C\u6D4B\u8BD5\u9A8C\u8BC1",
|
|
94
|
+
suggestion4: "\u4F7F\u7528 `/modernize` \u547D\u4EE4\u542F\u52A8\u4EA4\u4E92\u5F0F\u8FC1\u79FB\u5411\u5BFC"
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// src/reporters/analysis.ts
|
|
99
|
+
function renderAnalysisReport(report, locale = "zh") {
|
|
100
|
+
const t = LABELS[locale];
|
|
101
|
+
const lines = [];
|
|
102
|
+
lines.push(t.title);
|
|
103
|
+
lines.push("");
|
|
104
|
+
lines.push(t.overview);
|
|
105
|
+
lines.push("");
|
|
106
|
+
lines.push(`- **${t.projectRoot}**: \`${report.projectRoot}\``);
|
|
107
|
+
lines.push(`- **${t.scannedAt}**: ${report.scannedAt}`);
|
|
108
|
+
lines.push(`- **${t.totalFiles}**: ${report.totalFiles}`);
|
|
109
|
+
lines.push(`- **${t.totalPatterns}**: ${report.totalPatterns}`);
|
|
110
|
+
lines.push(`- **${t.duration}**: ${formatDuration(report.duration)}`);
|
|
111
|
+
lines.push("");
|
|
112
|
+
lines.push(t.dimensions);
|
|
113
|
+
lines.push("");
|
|
114
|
+
lines.push(
|
|
115
|
+
`| ${t.dimension} | ${t.count} | ${t.affectedFiles} | \u{1F534}${t.critical} | \u{1F7E1}${t.warning} | \u{1F535}${t.info} |`
|
|
116
|
+
);
|
|
117
|
+
lines.push("|------|--------|-----------|--------|--------|--------|");
|
|
118
|
+
for (const d of report.dimensions) {
|
|
119
|
+
lines.push(
|
|
120
|
+
`| ${dimensionLabel(d.dimension, locale)} | ${d.count} | ${d.files} | ${d.bySeverity.critical} | ${d.bySeverity.warning} | ${d.bySeverity.info} |`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
lines.push("");
|
|
124
|
+
lines.push(t.topPatterns);
|
|
125
|
+
lines.push("");
|
|
126
|
+
const sorted = [...report.patterns].sort((a, b) => {
|
|
127
|
+
const sev = { critical: 0, warning: 1, info: 2 };
|
|
128
|
+
return (sev[a.severity] ?? 9) - (sev[b.severity] ?? 9);
|
|
129
|
+
}).slice(0, 20);
|
|
130
|
+
for (const p of sorted) {
|
|
131
|
+
const badge = severityBadge(p.severity);
|
|
132
|
+
const dim = dimensionLabel(p.dimension, locale);
|
|
133
|
+
const loc = p.line > 0 ? `:${p.line}` : "";
|
|
134
|
+
lines.push(`${badge} **${p.name}** [${dim}] \`${p.file}${loc}\``);
|
|
135
|
+
if (p.suggestion) lines.push(` \u2192 ${p.suggestion}`);
|
|
136
|
+
}
|
|
137
|
+
lines.push("");
|
|
138
|
+
lines.push(t.riskAssessment);
|
|
139
|
+
lines.push("");
|
|
140
|
+
lines.push(`- **${t.riskLevel}**: **${report.risk.level.toUpperCase()}**`);
|
|
141
|
+
lines.push(`- **${t.reason}**: ${report.risk.reason}`);
|
|
142
|
+
lines.push(
|
|
143
|
+
`- **${t.recommendedOrder}**: ${report.risk.recommendedOrder.map((d) => dimensionLabel(d, locale)).join(" \u2192 ")}`
|
|
144
|
+
);
|
|
145
|
+
lines.push(`- **${t.estimatedEffort}**: ${report.risk.estimatedEffort} ${t.personDays}`);
|
|
146
|
+
lines.push("");
|
|
147
|
+
lines.push(t.suggestions);
|
|
148
|
+
lines.push("");
|
|
149
|
+
lines.push(`1. ${t.suggestion1}`);
|
|
150
|
+
lines.push(`2. ${t.suggestion2}`);
|
|
151
|
+
lines.push(`3. ${t.suggestion3}`);
|
|
152
|
+
lines.push(`4. ${t.suggestion4}`);
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/scanners/legacy-scanner.ts
|
|
157
|
+
import { glob, readFile } from "fs/promises";
|
|
158
|
+
import { join, relative, sep } from "path";
|
|
159
|
+
|
|
160
|
+
// src/types.ts
|
|
161
|
+
function createPatternLine(n) {
|
|
162
|
+
return n >= 0 ? n : 0;
|
|
163
|
+
}
|
|
164
|
+
var VERSION = "0.1.0";
|
|
165
|
+
|
|
166
|
+
// src/scanners/rules/eslint-modernize.ts
|
|
167
|
+
var eslintModernizeRules = [
|
|
168
|
+
{
|
|
169
|
+
id: "eslint-legacy-config",
|
|
170
|
+
name: "Legacy ESLint config format",
|
|
171
|
+
dimension: "eslint-modernize",
|
|
172
|
+
severity: "info",
|
|
173
|
+
regex: /module\.exports\s*=\s*\{[\s\S]*?(extends|parser|plugins|rules)\s*:/,
|
|
174
|
+
suggestion: "\u8FC1\u79FB\u4E3A ESLint 9 flat config (eslint.config.mjs)\uFF0C\u4F7F\u7528 export default \u66FF\u4EE3 module.exports"
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: "eslint-override-array",
|
|
178
|
+
name: "ESLint overrides array (flat config uses different pattern)",
|
|
179
|
+
dimension: "eslint-modernize",
|
|
180
|
+
severity: "info",
|
|
181
|
+
regex: /\boverrides\s*:\s*\[/,
|
|
182
|
+
suggestion: "ESLint flat config \u4E0D\u518D\u4F7F\u7528 overrides\uFF0C\u6539\u4E3A\u591A\u4E2A\u914D\u7F6E\u5BF9\u8C61\u7EC4\u5408"
|
|
183
|
+
}
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
// src/scanners/rules/jest-to-vitest.ts
|
|
187
|
+
var jestToVitestRules = [
|
|
188
|
+
{ id: "jest-config", name: "Jest config detected", dimension: "jest-to-vitest", severity: "info", regex: /jest\.config|from\s*['"]@jest|require\s*\(\s*['"]@jest/, suggestion: "\u8FC1\u79FB\u4E3A vitest.config.ts" },
|
|
189
|
+
{ id: "jest-global", name: "Jest global functions", dimension: "jest-to-vitest", severity: "warning", regex: /\b(describe|it|test|expect|beforeEach|afterEach|jest)\s*\(/, suggestion: "\u6539\u4E3A vitest \u5BFC\u5165 (vi, describe, it, expect)" }
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
// src/scanners/rules/js-to-ts.ts
|
|
193
|
+
var jsToTsRules = [
|
|
194
|
+
{ id: "js-prop-types", name: "PropTypes validation", dimension: "js-to-ts", severity: "warning", regex: /PropTypes\./, suggestion: "\u4F7F\u7528 TypeScript interface \u66FF\u4EE3 PropTypes" },
|
|
195
|
+
{ id: "js-jsdoc-types", name: "JSDoc type annotations", dimension: "js-to-ts", severity: "info", regex: /@type\s*\{/, suggestion: "\u5C06 JSDoc \u7C7B\u578B\u8FC1\u79FB\u4E3A\u5185\u8054 TypeScript \u7C7B\u578B" },
|
|
196
|
+
{ id: "js-commonjs-export", name: "CommonJS exports", dimension: "js-to-ts", severity: "info", regex: /module\.exports\s*=|exports\.\w+\s*=/, suggestion: "\u4F7F\u7528 ES module export \u66FF\u4EE3 CommonJS" },
|
|
197
|
+
{ id: "js-require-import", name: "require() import", dimension: "js-to-ts", severity: "info", regex: /\brequire\s*\(\s*['"][^'"]+['"]\s*\)/, suggestion: "\u4F7F\u7528 ES import \u66FF\u4EE3 require()" }
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
// src/scanners/rules/vue2-to-vue3.ts
|
|
201
|
+
var vue2ToVue3Rules = [
|
|
202
|
+
{ id: "vue2-options-api", name: "Options API component", dimension: "vue2-to-vue3", severity: "info", regex: /export\s+default\s*\{[\s\S]*?data\s*\(\)/, suggestion: "\u4F7F\u7528 <script setup> + Composition API \u91CD\u5199" },
|
|
203
|
+
{ id: "vue2-data-function", name: "data() option", dimension: "vue2-to-vue3", severity: "info", regex: /\bdata\s*\(\)\s*\{/, suggestion: "\u4F7F\u7528 ref() / reactive() \u66FF\u4EE3" },
|
|
204
|
+
{ id: "vue2-computed-option", name: "computed:{} option", dimension: "vue2-to-vue3", severity: "info", regex: /\bcomputed\s*:\s*\{/, suggestion: "\u4F7F\u7528 computed(() => ...) \u66FF\u4EE3" },
|
|
205
|
+
{ id: "vue2-methods-option", name: "methods:{} option", dimension: "vue2-to-vue3", severity: "info", regex: /\bmethods\s*:\s*\{/, suggestion: "\u6539\u5199\u4E3A\u666E\u901A\u51FD\u6570" },
|
|
206
|
+
{ id: "vue2-watch-option", name: "watch:{} option", dimension: "vue2-to-vue3", severity: "info", regex: /\bwatch\s*:\s*\{/, suggestion: "\u4F7F\u7528 watch() / watchEffect() \u66FF\u4EE3" },
|
|
207
|
+
{ id: "vue2-this-refs", name: "this.$refs", dimension: "vue2-to-vue3", severity: "warning", regex: /this\.\$refs\./, suggestion: "\u4F7F\u7528 useTemplateRef() \u66FF\u4EE3" },
|
|
208
|
+
{ id: "vue2-this-emit", name: "this.$emit", dimension: "vue2-to-vue3", severity: "warning", regex: /this\.\$emit/, suggestion: "\u4F7F\u7528 defineEmits() \u66FF\u4EE3" },
|
|
209
|
+
{ id: "vue2-this-router", name: "this.$router / this.$route", dimension: "vue2-to-vue3", severity: "warning", regex: /this\.\$(router|route)\b/, suggestion: "\u4F7F\u7528 useRouter() / useRoute() \u66FF\u4EE3" },
|
|
210
|
+
{ id: "vue2-this-store", name: "this.$store", dimension: "vue2-to-vue3", severity: "warning", regex: /this\.\$store/, suggestion: "\u4F7F\u7528 useStore() \u6216\u8FC1\u79FB\u5230 Pinia" },
|
|
211
|
+
{ id: "vue2-filters", name: "filters:{} option", dimension: "vue2-to-vue3", severity: "critical", regex: /\bfilters\s*:\s*\{/, suggestion: "Vue 3 \u5DF2\u79FB\u9664 filters\uFF0C\u6539\u4E3A computed \u6216\u65B9\u6CD5" },
|
|
212
|
+
{ id: "vue2-event-bus", name: "$on / $off / $once", dimension: "vue2-to-vue3", severity: "critical", regex: /\.\$(on|off|once)\s*\(/, suggestion: "Vue 3 \u5DF2\u79FB\u9664\u4E8B\u4EF6\u603B\u7EBF\uFF0C\u4F7F\u7528 mitt \u6216 tiny-emitter" },
|
|
213
|
+
{ id: "vue2-destroyed", name: "destroyed / beforeDestroy", dimension: "vue2-to-vue3", severity: "warning", regex: /\b(destroyed|beforeDestroy)\s*[:(]/, suggestion: "\u6539\u4E3A unmounted / beforeUnmount" },
|
|
214
|
+
{ id: "vue2-mixins", name: "mixins:[] option", dimension: "vue2-to-vue3", severity: "warning", regex: /\bmixins\s*:\s*\[/, suggestion: "\u4F7F\u7528 Composables \u66FF\u4EE3 mixins" },
|
|
215
|
+
{ id: "vue2-v-bind-sync", name: "v-bind.sync modifier", dimension: "vue2-to-vue3", severity: "critical", regex: /v-bind\.\w+\.sync/, suggestion: "\u6539\u4E3A v-model:xxx (Vue 3 \u79FB\u9664\u4E86 .sync)" },
|
|
216
|
+
{ id: "vue2-v-on-native", name: "v-on.native modifier", dimension: "vue2-to-vue3", severity: "warning", regex: /v-on\.native|@\.native/, suggestion: "Vue 3 \u79FB\u9664\u4E86 .native\uFF0C\u5728 emits \u4E2D\u58F0\u660E\u5373\u53EF" }
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
// src/scanners/rules/vuex-to-pinia.ts
|
|
220
|
+
var vuexToPiniaRules = [
|
|
221
|
+
{ id: "vuex-store", name: "Vuex store usage", dimension: "vuex-to-pinia", severity: "info", regex: /new\s+Vuex\.Store|from\s*['"]vuex['"]/, suggestion: "\u8FC1\u79FB\u4E3A Pinia defineStore()" },
|
|
222
|
+
{ id: "vuex-map-state", name: "mapState / mapGetters", dimension: "vuex-to-pinia", severity: "warning", regex: /map(State|Getters|Mutations|Actions)\s*\(/, suggestion: "Pinia \u4F7F\u7528 storeToRefs() \u66FF\u4EE3 map \u8F85\u52A9\u51FD\u6570" }
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
// src/scanners/rules/webpack-to-vite.ts
|
|
226
|
+
var webpackToViteRules = [
|
|
227
|
+
{ id: "webpack-config", name: "webpack.config file", dimension: "webpack-to-vite", severity: "info", regex: /require\s*\(\s*['"]webpack['"]\s*\)/, suggestion: "\u8FC1\u79FB\u4E3A vite.config.ts" },
|
|
228
|
+
{ id: "webpack-require-context", name: "require.context", dimension: "webpack-to-vite", severity: "warning", regex: /require\.context\s*\(/, suggestion: "Vite \u4F7F\u7528 import.meta.glob \u66FF\u4EE3" },
|
|
229
|
+
{ id: "webpack-process-env", name: "process.env usage", dimension: "webpack-to-vite", severity: "info", regex: /process\.env\./, suggestion: "Vite \u4F7F\u7528 import.meta.env \u66FF\u4EE3" }
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
// src/scanners/rules/index.ts
|
|
233
|
+
var PATTERN_RULES = [
|
|
234
|
+
...vue2ToVue3Rules,
|
|
235
|
+
...jsToTsRules,
|
|
236
|
+
...webpackToViteRules,
|
|
237
|
+
...jestToVitestRules,
|
|
238
|
+
...vuexToPiniaRules,
|
|
239
|
+
...eslintModernizeRules
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
// src/scanners/legacy-scanner.ts
|
|
243
|
+
var DEFAULT_RISK_THRESHOLDS = {
|
|
244
|
+
criticalHigh: 20,
|
|
245
|
+
criticalMedium: 5,
|
|
246
|
+
warningMedium: 30,
|
|
247
|
+
filesHigh: 200,
|
|
248
|
+
filesMedium: 50,
|
|
249
|
+
effortHigh: 15,
|
|
250
|
+
effortMedium: 25,
|
|
251
|
+
effortLow: 40
|
|
252
|
+
};
|
|
253
|
+
var DEFAULT_MAX_MATCHES_PER_RULE = 10;
|
|
254
|
+
var CONCURRENT_READ_LIMIT = 50;
|
|
255
|
+
function isExcluded(rel, excludes) {
|
|
256
|
+
const segments = rel.split(sep);
|
|
257
|
+
for (const pattern of excludes) {
|
|
258
|
+
const parts = pattern.split("/");
|
|
259
|
+
if (matchGlobSegments(segments, parts, 0, 0)) return true;
|
|
260
|
+
}
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
function matchGlobSegments(pathSegs, patternParts, pi, ppi) {
|
|
264
|
+
if (ppi === patternParts.length) return pi === pathSegs.length;
|
|
265
|
+
const pat = patternParts[ppi];
|
|
266
|
+
if (pat === "**") {
|
|
267
|
+
let nextPPi = ppi + 1;
|
|
268
|
+
while (nextPPi < patternParts.length && patternParts[nextPPi] === "**") nextPPi++;
|
|
269
|
+
if (nextPPi === patternParts.length) return true;
|
|
270
|
+
for (let i = pi; i <= pathSegs.length; i++) {
|
|
271
|
+
if (matchGlobSegments(pathSegs, patternParts, i, nextPPi)) return true;
|
|
272
|
+
}
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
if (pi === pathSegs.length) return false;
|
|
276
|
+
const seg = pathSegs[pi];
|
|
277
|
+
if (pat === "*") {
|
|
278
|
+
return matchGlobSegments(pathSegs, patternParts, pi + 1, ppi + 1);
|
|
279
|
+
}
|
|
280
|
+
if (seg === pat) {
|
|
281
|
+
return matchGlobSegments(pathSegs, patternParts, pi + 1, ppi + 1);
|
|
282
|
+
}
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
function extractScriptContent(content, filePath) {
|
|
286
|
+
if (!filePath.endsWith(".vue")) return content;
|
|
287
|
+
let result = content.replace(/<template[\s\S]*?<\/template>/gi, "");
|
|
288
|
+
result = result.replace(/<style[\s\S]*?<\/style>/gi, "");
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
async function readFilesConcurrently(filePaths, root, limit) {
|
|
292
|
+
const results = /* @__PURE__ */ new Map();
|
|
293
|
+
const batch = [];
|
|
294
|
+
for (const rel of filePaths) {
|
|
295
|
+
const p = readFile(join(root, rel), "utf-8").then((content) => {
|
|
296
|
+
results.set(rel, content);
|
|
297
|
+
}).catch(() => {
|
|
298
|
+
});
|
|
299
|
+
batch.push(p);
|
|
300
|
+
if (batch.length >= limit) {
|
|
301
|
+
await Promise.allSettled(batch);
|
|
302
|
+
batch.length = 0;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (batch.length > 0) {
|
|
306
|
+
await Promise.allSettled(batch);
|
|
307
|
+
}
|
|
308
|
+
return results;
|
|
309
|
+
}
|
|
310
|
+
function scanFileContent(content, filePath, maxMatchesPerRule = DEFAULT_MAX_MATCHES_PER_RULE) {
|
|
311
|
+
const scanContent = extractScriptContent(content, filePath);
|
|
312
|
+
const patterns = [];
|
|
313
|
+
for (const rule of PATTERN_RULES) {
|
|
314
|
+
if (rule.dimension === "js-to-ts" && /\.(ts|tsx)$/.test(filePath)) continue;
|
|
315
|
+
const flags = rule.regex.flags.includes("g") ? rule.regex.flags : `${rule.regex.flags}g`;
|
|
316
|
+
const regex = new RegExp(rule.regex.source, flags);
|
|
317
|
+
let match = regex.exec(scanContent), matchCount = 0;
|
|
318
|
+
while (match !== null && matchCount < maxMatchesPerRule) {
|
|
319
|
+
const line = scanContent.slice(0, match.index).split("\n").length;
|
|
320
|
+
patterns.push({
|
|
321
|
+
id: rule.id,
|
|
322
|
+
name: rule.name,
|
|
323
|
+
dimension: rule.dimension,
|
|
324
|
+
severity: rule.severity,
|
|
325
|
+
file: filePath,
|
|
326
|
+
line: createPatternLine(line),
|
|
327
|
+
snippet: match[0].slice(0, 120),
|
|
328
|
+
suggestion: rule.suggestion
|
|
329
|
+
});
|
|
330
|
+
matchCount++;
|
|
331
|
+
if (match[0].length === 0) regex.lastIndex++;
|
|
332
|
+
match = regex.exec(scanContent);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return patterns;
|
|
336
|
+
}
|
|
337
|
+
async function scanProject(options) {
|
|
338
|
+
const startTime = performance.now();
|
|
339
|
+
const root = options.root;
|
|
340
|
+
const include = options.include ?? ["**/*.{vue,js,ts,jsx,tsx}"];
|
|
341
|
+
const exclude = options.exclude ?? ["node_modules/**", "dist/**", "coverage/**", ".git/**"];
|
|
342
|
+
const maxPerRule = options.maxMatchesPerRule ?? DEFAULT_MAX_MATCHES_PER_RULE;
|
|
343
|
+
const thresholds = { ...DEFAULT_RISK_THRESHOLDS, ...options.riskThresholds };
|
|
344
|
+
const files = [];
|
|
345
|
+
for (const pattern of include) {
|
|
346
|
+
const globIter = glob(join(root, pattern));
|
|
347
|
+
for await (const file of globIter) {
|
|
348
|
+
const rel = relative(root, file);
|
|
349
|
+
if (!isExcluded(rel, exclude)) {
|
|
350
|
+
files.push(rel);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (options.maxFiles && options.maxFiles > 0) {
|
|
355
|
+
files.splice(options.maxFiles);
|
|
356
|
+
}
|
|
357
|
+
const contents = await readFilesConcurrently(files, root, CONCURRENT_READ_LIMIT);
|
|
358
|
+
const allPatterns = [];
|
|
359
|
+
let totalFiles = 0, jsFiles = 0;
|
|
360
|
+
for (const [rel, content] of contents) {
|
|
361
|
+
const patterns = scanFileContent(content, rel, maxPerRule);
|
|
362
|
+
allPatterns.push(...patterns);
|
|
363
|
+
totalFiles++;
|
|
364
|
+
if (/\.(js|jsx|vue)$/.test(rel)) jsFiles++;
|
|
365
|
+
}
|
|
366
|
+
const dimensions = computeDimensionStats(allPatterns, jsFiles);
|
|
367
|
+
const risk = assessRisk(allPatterns, dimensions, totalFiles, thresholds);
|
|
368
|
+
return {
|
|
369
|
+
projectRoot: root,
|
|
370
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
371
|
+
totalFiles,
|
|
372
|
+
totalPatterns: allPatterns.length,
|
|
373
|
+
patterns: allPatterns,
|
|
374
|
+
dimensions,
|
|
375
|
+
risk,
|
|
376
|
+
duration: Math.round(performance.now() - startTime)
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function computeDimensionStats(patterns, jsFiles) {
|
|
380
|
+
const map = /* @__PURE__ */ new Map();
|
|
381
|
+
for (const p of patterns) {
|
|
382
|
+
let entry = map.get(p.dimension);
|
|
383
|
+
if (!entry) {
|
|
384
|
+
entry = { count: 0, files: /* @__PURE__ */ new Set(), bySeverity: { critical: 0, warning: 0, info: 0 } };
|
|
385
|
+
map.set(p.dimension, entry);
|
|
386
|
+
}
|
|
387
|
+
entry.count++;
|
|
388
|
+
entry.bySeverity[p.severity]++;
|
|
389
|
+
entry.files.add(p.file);
|
|
390
|
+
}
|
|
391
|
+
const result = Array.from(map, ([dimension, e]) => ({
|
|
392
|
+
dimension,
|
|
393
|
+
count: e.count,
|
|
394
|
+
files: e.files.size,
|
|
395
|
+
bySeverity: e.bySeverity
|
|
396
|
+
}));
|
|
397
|
+
if (jsFiles > 0) {
|
|
398
|
+
const existing = result.find((d) => d.dimension === "js-to-ts");
|
|
399
|
+
if (existing) {
|
|
400
|
+
existing.files = Math.max(existing.files, jsFiles);
|
|
401
|
+
existing.count = Math.max(existing.count, jsFiles);
|
|
402
|
+
} else {
|
|
403
|
+
result.push({
|
|
404
|
+
dimension: "js-to-ts",
|
|
405
|
+
count: jsFiles,
|
|
406
|
+
files: jsFiles,
|
|
407
|
+
bySeverity: { critical: 0, warning: 0, info: jsFiles }
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return result;
|
|
412
|
+
}
|
|
413
|
+
function assessRisk(patterns, dimensions, totalFiles, thresholds) {
|
|
414
|
+
const criticalCount = patterns.filter((p) => p.severity === "critical").length;
|
|
415
|
+
const warningCount = patterns.filter((p) => p.severity === "warning").length;
|
|
416
|
+
let level, estimatedEffort;
|
|
417
|
+
if (criticalCount > thresholds.criticalHigh || totalFiles > thresholds.filesHigh) {
|
|
418
|
+
level = "high";
|
|
419
|
+
estimatedEffort = Math.ceil(totalFiles / thresholds.effortHigh);
|
|
420
|
+
} else if (criticalCount > thresholds.criticalMedium || warningCount > thresholds.warningMedium || totalFiles > thresholds.filesMedium) {
|
|
421
|
+
level = "medium";
|
|
422
|
+
estimatedEffort = Math.ceil(totalFiles / thresholds.effortMedium);
|
|
423
|
+
} else {
|
|
424
|
+
level = "low";
|
|
425
|
+
estimatedEffort = Math.max(1, Math.ceil(totalFiles / thresholds.effortLow));
|
|
426
|
+
}
|
|
427
|
+
const recommendedOrder = [...dimensions].sort((a, b) => {
|
|
428
|
+
const severityScore = (s) => s.bySeverity.critical * 3 + s.bySeverity.warning;
|
|
429
|
+
return severityScore(b) - severityScore(a);
|
|
430
|
+
}).map((d) => d.dimension);
|
|
431
|
+
const reason = criticalCount > 0 ? `\u53D1\u73B0 ${criticalCount} \u4E2A\u4E25\u91CD\u95EE\u9898\u9700\u8981\u4F18\u5148\u5904\u7406\uFF08\u5982\u5DF2\u5E9F\u5F03 API\u3001\u7834\u574F\u6027\u53D8\u66F4\uFF09\uFF0C${warningCount} \u4E2A\u8B66\u544A` : warningCount > 0 ? `\u53D1\u73B0 ${warningCount} \u4E2A\u8B66\u544A\u7EA7\u522B\u95EE\u9898\uFF0C\u5EFA\u8BAE\u9010\u6B65\u8FC1\u79FB` : "\u9879\u76EE\u73B0\u4EE3\u5316\u7A0B\u5EA6\u8F83\u597D\uFF0C\u4EC5\u9700\u5C11\u91CF\u8C03\u6574";
|
|
432
|
+
return { level, reason, recommendedOrder, estimatedEffort };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// src/utils/config.ts
|
|
436
|
+
var DEFAULT_INCLUDES = {
|
|
437
|
+
"vue2-to-vue3": ["**/*.vue", "**/*.js", "**/*.ts"],
|
|
438
|
+
"js-to-ts": ["**/*.js", "**/*.jsx", "**/*.vue"],
|
|
439
|
+
"webpack-to-vite": ["webpack.config.*", "**/webpack.*.config.*"],
|
|
440
|
+
"jest-to-vitest": ["jest.config.*", "**/*.test.*", "**/*.spec.*"],
|
|
441
|
+
"vuex-to-pinia": ["**/store/**", "**/vuex/**", "**/*.vue"],
|
|
442
|
+
"eslint-modernize": [".eslintrc.*", "eslint.config.*"]
|
|
443
|
+
};
|
|
444
|
+
var DEFAULT_DIMENSION = {
|
|
445
|
+
enabled: true,
|
|
446
|
+
include: [],
|
|
447
|
+
exclude: ["node_modules/**", "dist/**", "coverage/**"]
|
|
448
|
+
};
|
|
449
|
+
var DEFAULT_CONFIG = {
|
|
450
|
+
dimensions: {
|
|
451
|
+
"vue2-to-vue3": { ...DEFAULT_DIMENSION, include: DEFAULT_INCLUDES["vue2-to-vue3"] },
|
|
452
|
+
"js-to-ts": { ...DEFAULT_DIMENSION, include: DEFAULT_INCLUDES["js-to-ts"] },
|
|
453
|
+
"webpack-to-vite": { ...DEFAULT_DIMENSION, include: DEFAULT_INCLUDES["webpack-to-vite"] },
|
|
454
|
+
"jest-to-vitest": { ...DEFAULT_DIMENSION, include: DEFAULT_INCLUDES["jest-to-vitest"] },
|
|
455
|
+
"vuex-to-pinia": { ...DEFAULT_DIMENSION, include: DEFAULT_INCLUDES["vuex-to-pinia"] },
|
|
456
|
+
"eslint-modernize": { ...DEFAULT_DIMENSION, include: DEFAULT_INCLUDES["eslint-modernize"] }
|
|
457
|
+
},
|
|
458
|
+
maxFiles: 0,
|
|
459
|
+
includeSuggestions: true
|
|
460
|
+
};
|
|
461
|
+
var VALID_DIMENSIONS = new Set(Object.keys(DEFAULT_CONFIG.dimensions));
|
|
462
|
+
function mergeConfig(user) {
|
|
463
|
+
const merged = {
|
|
464
|
+
...DEFAULT_CONFIG,
|
|
465
|
+
...user,
|
|
466
|
+
dimensions: { ...DEFAULT_CONFIG.dimensions }
|
|
467
|
+
};
|
|
468
|
+
if (user.dimensions) {
|
|
469
|
+
for (const dim of Object.keys(user.dimensions)) {
|
|
470
|
+
if (!VALID_DIMENSIONS.has(dim)) {
|
|
471
|
+
console.warn(`[legacy-modernizer] Unknown dimension key "${dim}" in config \u2014 skipping`);
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
merged.dimensions[dim] = {
|
|
475
|
+
...DEFAULT_CONFIG.dimensions[dim],
|
|
476
|
+
...user.dimensions[dim]
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return merged;
|
|
481
|
+
}
|
|
482
|
+
function getDefaultConfig() {
|
|
483
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
484
|
+
}
|
|
485
|
+
export {
|
|
486
|
+
VERSION,
|
|
487
|
+
createPatternLine,
|
|
488
|
+
dimensionLabel,
|
|
489
|
+
formatDuration,
|
|
490
|
+
getDefaultConfig,
|
|
491
|
+
mergeConfig,
|
|
492
|
+
patternToMarkdown,
|
|
493
|
+
renderAnalysisReport,
|
|
494
|
+
scanFileContent,
|
|
495
|
+
scanProject,
|
|
496
|
+
severityBadge
|
|
497
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analysis report renderer — produces full Markdown from an AnalysisReport
|
|
3
|
+
* Fix #3: locale parameter for i18n output
|
|
4
|
+
*/
|
|
5
|
+
import type { AnalysisReport } from '../types';
|
|
6
|
+
import { type Locale } from './locale';
|
|
7
|
+
/** Render a full AnalysisReport into Markdown */
|
|
8
|
+
export declare function renderAnalysisReport(report: AnalysisReport, locale?: Locale): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy pattern scanner — detects Vue 2 / JS / Webpack legacy patterns using AI-semantic rules
|
|
3
|
+
*/
|
|
4
|
+
import type { AnalysisReport, LegacyPattern, ScannerOptions } from '../types';
|
|
5
|
+
/**
|
|
6
|
+
* Scan a single file for legacy patterns.
|
|
7
|
+
* Fix #5: strips <style>/<template> from .vue files before scanning.
|
|
8
|
+
*/
|
|
9
|
+
export declare function scanFileContent(content: string, filePath: string, maxMatchesPerRule?: number): LegacyPattern[];
|
|
10
|
+
/**
|
|
11
|
+
* Scan a project directory and produce a full analysis report.
|
|
12
|
+
* Fix #7: concurrent file reads.
|
|
13
|
+
* Fix #4: simplified JS file counting.
|
|
14
|
+
* Fix #6: configurable risk thresholds.
|
|
15
|
+
*/
|
|
16
|
+
export declare function scanProject(options: ScannerOptions): Promise<AnalysisReport>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|