seo-gravity-mcp 1.0.2 → 1.2.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 +118 -105
- package/dist/adapters/adapterRegistry.d.ts +7 -0
- package/dist/adapters/adapterRegistry.js +30 -0
- package/dist/adapters/astroAdapter.d.ts +17 -0
- package/dist/adapters/astroAdapter.js +191 -0
- package/dist/adapters/nextAppAdapter.d.ts +17 -0
- package/dist/adapters/nextAppAdapter.js +265 -0
- package/dist/adapters/nextPagesAdapter.d.ts +17 -0
- package/dist/adapters/nextPagesAdapter.js +209 -0
- package/dist/adapters/remixAdapter.d.ts +16 -0
- package/dist/adapters/remixAdapter.js +118 -0
- package/dist/adapters/staticAdapter.d.ts +16 -0
- package/dist/adapters/staticAdapter.js +117 -0
- package/dist/adapters/svelteKitAdapter.d.ts +16 -0
- package/dist/adapters/svelteKitAdapter.js +122 -0
- package/dist/adapters/types.d.ts +39 -0
- package/dist/adapters/types.js +1 -0
- package/dist/adapters/viteReactAdapter.d.ts +16 -0
- package/dist/adapters/viteReactAdapter.js +189 -0
- package/dist/benchmark/correlationBenchmark.d.ts +1 -0
- package/dist/benchmark/correlationBenchmark.js +96 -0
- package/dist/benchmark/invariantBenchmark.d.ts +1 -0
- package/dist/benchmark/invariantBenchmark.js +47 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +162 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +8 -646
- package/dist/invariants/registry.d.ts +12 -0
- package/dist/invariants/registry.js +178 -0
- package/dist/invariants/types.d.ts +38 -0
- package/dist/invariants/types.js +1 -0
- package/dist/providers/providerRegistry.d.ts +65 -0
- package/dist/providers/providerRegistry.js +64 -0
- package/dist/providers/types.d.ts +22 -0
- package/dist/providers/types.js +1 -0
- package/dist/server/registry.d.ts +3 -0
- package/dist/server/registry.js +559 -0
- package/dist/server/server.d.ts +70 -0
- package/dist/server/server.js +53 -0
- package/dist/server/types.d.ts +7 -0
- package/dist/server/types.js +1 -0
- package/dist/test.js +108 -138
- package/dist/tools/orchestration.js +23 -21
- package/dist/types/canonical.d.ts +64 -0
- package/dist/types/canonical.js +1 -0
- package/dist/types/findings.d.ts +22 -0
- package/dist/types/findings.js +1 -1
- package/dist/types/seo.d.ts +24 -0
- package/dist/utils/astLocator.d.ts +14 -0
- package/dist/utils/astLocator.js +137 -0
- package/dist/utils/cacheManager.d.ts +29 -0
- package/dist/utils/cacheManager.js +60 -0
- package/dist/utils/experimentEngine.d.ts +26 -0
- package/dist/utils/experimentEngine.js +41 -0
- package/dist/utils/findingEngine.d.ts +2 -1
- package/dist/utils/findingEngine.js +1 -0
- package/dist/utils/gitDiffEngine.d.ts +28 -0
- package/dist/utils/gitDiffEngine.js +109 -0
- package/dist/utils/opportunityEngine.d.ts +19 -0
- package/dist/utils/opportunityEngine.js +61 -0
- package/dist/utils/projectScanner.d.ts +4 -2
- package/dist/utils/projectScanner.js +14 -346
- package/dist/utils/sarifExporter.d.ts +64 -0
- package/dist/utils/sarifExporter.js +81 -0
- package/dist/utils/snapshotEngine.d.ts +3 -1
- package/dist/utils/snapshotEngine.js +232 -94
- package/package.json +8 -5
|
@@ -1,353 +1,21 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
1
|
import * as path from 'path';
|
|
3
|
-
|
|
2
|
+
import { defaultAdapterRegistry } from '../adapters/adapterRegistry.js';
|
|
3
|
+
export function getProjectAdapter(projectDir) {
|
|
4
4
|
const resolvedDir = path.resolve(projectDir);
|
|
5
|
-
|
|
6
|
-
let pkg = {};
|
|
7
|
-
if (fs.existsSync(pkgPath)) {
|
|
8
|
-
try {
|
|
9
|
-
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
10
|
-
}
|
|
11
|
-
catch {
|
|
12
|
-
// Ignored
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
const allDeps = {
|
|
16
|
-
...(pkg.dependencies || {}),
|
|
17
|
-
...(pkg.devDependencies || {})
|
|
18
|
-
};
|
|
19
|
-
const hasFile = (relPath) => fs.existsSync(path.join(resolvedDir, relPath));
|
|
20
|
-
const hasDir = (relPath) => {
|
|
21
|
-
const p = path.join(resolvedDir, relPath);
|
|
22
|
-
return fs.existsSync(p) && fs.statSync(p).isDirectory();
|
|
23
|
-
};
|
|
24
|
-
let framework = 'unknown';
|
|
25
|
-
let routesDir;
|
|
26
|
-
let devCommand = pkg.scripts?.dev ? 'npm run dev' : undefined;
|
|
27
|
-
let defaultDevPort = 3000;
|
|
28
|
-
// Next.js App Router vs Pages Router
|
|
29
|
-
if (allDeps['next']) {
|
|
30
|
-
if (hasDir('app') || hasDir('src/app')) {
|
|
31
|
-
framework = 'nextjs-app-router';
|
|
32
|
-
routesDir = hasDir('app') ? 'app' : 'src/app';
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
framework = 'nextjs-pages-router';
|
|
36
|
-
routesDir = hasDir('pages') ? 'pages' : 'src/pages';
|
|
37
|
-
}
|
|
38
|
-
defaultDevPort = 3000;
|
|
39
|
-
}
|
|
40
|
-
// Astro
|
|
41
|
-
else if (allDeps['astro'] || hasFile('astro.config.mjs') || hasFile('astro.config.ts')) {
|
|
42
|
-
framework = 'astro';
|
|
43
|
-
routesDir = hasDir('src/pages') ? 'src/pages' : 'pages';
|
|
44
|
-
defaultDevPort = 4321;
|
|
45
|
-
}
|
|
46
|
-
// Remix
|
|
47
|
-
else if (allDeps['@remix-run/react'] || allDeps['@remix-run/node']) {
|
|
48
|
-
framework = 'remix';
|
|
49
|
-
routesDir = hasDir('app/routes') ? 'app/routes' : 'routes';
|
|
50
|
-
defaultDevPort = 3000;
|
|
51
|
-
}
|
|
52
|
-
// SvelteKit
|
|
53
|
-
else if (allDeps['@sveltejs/kit']) {
|
|
54
|
-
framework = 'sveltekit';
|
|
55
|
-
routesDir = hasDir('src/routes') ? 'src/routes' : 'routes';
|
|
56
|
-
defaultDevPort = 5173;
|
|
57
|
-
}
|
|
58
|
-
// Nuxt
|
|
59
|
-
else if (allDeps['nuxt'] || allDeps['nuxt3']) {
|
|
60
|
-
framework = 'nuxt';
|
|
61
|
-
routesDir = hasDir('pages') ? 'pages' : 'src/pages';
|
|
62
|
-
defaultDevPort = 3000;
|
|
63
|
-
}
|
|
64
|
-
// Vite / React SPA
|
|
65
|
-
else if (allDeps['vite'] || allDeps['react']) {
|
|
66
|
-
framework = 'vite-react';
|
|
67
|
-
routesDir = hasDir('src') ? 'src' : '.';
|
|
68
|
-
defaultDevPort = 5173;
|
|
69
|
-
}
|
|
70
|
-
// Static HTML
|
|
71
|
-
else if (hasFile('index.html')) {
|
|
72
|
-
framework = 'static-html';
|
|
73
|
-
routesDir = '.';
|
|
74
|
-
defaultDevPort = 8080;
|
|
75
|
-
}
|
|
76
|
-
// Detect sitemap, robots, llms.txt
|
|
77
|
-
const hasSitemapConfig = hasFile('public/sitemap.xml') ||
|
|
78
|
-
hasFile('sitemap.xml') ||
|
|
79
|
-
hasFile('app/sitemap.ts') ||
|
|
80
|
-
hasFile('app/sitemap.js') ||
|
|
81
|
-
hasFile('src/app/sitemap.ts') ||
|
|
82
|
-
hasFile('src/app/sitemap.js') ||
|
|
83
|
-
hasFile('src/pages/sitemap.xml.ts');
|
|
84
|
-
const hasRobotsConfig = hasFile('public/robots.txt') ||
|
|
85
|
-
hasFile('robots.txt') ||
|
|
86
|
-
hasFile('app/robots.ts') ||
|
|
87
|
-
hasFile('app/robots.js') ||
|
|
88
|
-
hasFile('src/app/robots.ts') ||
|
|
89
|
-
hasFile('src/app/robots.js');
|
|
90
|
-
const hasLlmsTxt = hasFile('public/llms.txt') ||
|
|
91
|
-
hasFile('llms.txt') ||
|
|
92
|
-
hasFile('public/llms-full.txt') ||
|
|
93
|
-
hasFile('llms-full.txt');
|
|
94
|
-
const hasTypeScript = hasFile('tsconfig.json') ||
|
|
95
|
-
Boolean(allDeps['typescript']);
|
|
96
|
-
// Package manager detection
|
|
97
|
-
let packageManager = 'npm';
|
|
98
|
-
if (hasFile('pnpm-lock.yaml'))
|
|
99
|
-
packageManager = 'pnpm';
|
|
100
|
-
else if (hasFile('yarn.lock'))
|
|
101
|
-
packageManager = 'yarn';
|
|
102
|
-
else if (hasFile('bun.lockb'))
|
|
103
|
-
packageManager = 'bun';
|
|
104
|
-
return {
|
|
105
|
-
framework,
|
|
106
|
-
name: pkg.name || path.basename(resolvedDir),
|
|
107
|
-
version: pkg.version,
|
|
108
|
-
packageManager,
|
|
109
|
-
hasTypeScript,
|
|
110
|
-
hasSitemapConfig,
|
|
111
|
-
hasRobotsConfig,
|
|
112
|
-
hasLlmsTxt,
|
|
113
|
-
rootDir: resolvedDir,
|
|
114
|
-
routesDir,
|
|
115
|
-
devCommand,
|
|
116
|
-
defaultDevPort
|
|
117
|
-
};
|
|
5
|
+
return defaultAdapterRegistry.getAdapterForProject(resolvedDir);
|
|
118
6
|
}
|
|
119
|
-
export function
|
|
7
|
+
export function detectFramework(projectDir) {
|
|
120
8
|
const resolvedDir = path.resolve(projectDir);
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
if (!info.routesDir)
|
|
124
|
-
return discovered;
|
|
125
|
-
const fullRoutesDir = path.join(resolvedDir, info.routesDir);
|
|
126
|
-
if (!fs.existsSync(fullRoutesDir))
|
|
127
|
-
return discovered;
|
|
128
|
-
const readRouteFileContent = (relFile) => {
|
|
129
|
-
try {
|
|
130
|
-
const fullPath = path.join(resolvedDir, relFile);
|
|
131
|
-
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
132
|
-
return {
|
|
133
|
-
hasMetadataExport: /export\s+const\s+metadata\b/.test(content),
|
|
134
|
-
hasGenerateMetadata: /export\s+(async\s+)?function\s+generateMetadata\b/.test(content),
|
|
135
|
-
hasSchemaMarkup: /application\/ld\+json/.test(content) || /schema\.org/.test(content),
|
|
136
|
-
hasHeadComponent: /<Head\b|<Helmet\b|astro:head|<meta\b|<title\b/.test(content),
|
|
137
|
-
isClientComponent: /['"]use client['"]/.test(content)
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
return {
|
|
142
|
-
hasMetadataExport: false,
|
|
143
|
-
hasGenerateMetadata: false,
|
|
144
|
-
hasSchemaMarkup: false,
|
|
145
|
-
hasHeadComponent: false,
|
|
146
|
-
isClientComponent: false
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
// Next.js App Router Scanner
|
|
151
|
-
if (info.framework === 'nextjs-app-router') {
|
|
152
|
-
const scanAppDir = (currentDir, routePrefix) => {
|
|
153
|
-
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
154
|
-
for (const entry of entries) {
|
|
155
|
-
const entryPath = path.join(currentDir, entry.name);
|
|
156
|
-
if (entry.isDirectory()) {
|
|
157
|
-
// Ignore route groups like (marketing) in path
|
|
158
|
-
const segment = entry.name.startsWith('(') && entry.name.endsWith(')') ? '' : entry.name;
|
|
159
|
-
const nextPrefix = segment ? (routePrefix === '/' ? `/${segment}` : `${routePrefix}/${segment}`) : routePrefix;
|
|
160
|
-
scanAppDir(entryPath, nextPrefix);
|
|
161
|
-
}
|
|
162
|
-
else if (entry.isFile()) {
|
|
163
|
-
if (/^page\.(tsx|jsx|js|ts)$/.test(entry.name)) {
|
|
164
|
-
const relFile = path.relative(resolvedDir, entryPath).replace(/\\/g, '/');
|
|
165
|
-
const routePath = routePrefix === '' ? '/' : routePrefix;
|
|
166
|
-
const dynamicParams = extractDynamicParams(routePath);
|
|
167
|
-
const meta = readRouteFileContent(relFile);
|
|
168
|
-
discovered.push({
|
|
169
|
-
routePath,
|
|
170
|
-
sourceFilePath: relFile,
|
|
171
|
-
isDynamic: dynamicParams.length > 0,
|
|
172
|
-
dynamicParams,
|
|
173
|
-
routeType: 'page',
|
|
174
|
-
...meta
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
else if (/^sitemap\.(ts|js|xml)$/.test(entry.name)) {
|
|
178
|
-
const relFile = path.relative(resolvedDir, entryPath).replace(/\\/g, '/');
|
|
179
|
-
discovered.push({
|
|
180
|
-
routePath: '/sitemap.xml',
|
|
181
|
-
sourceFilePath: relFile,
|
|
182
|
-
isDynamic: false,
|
|
183
|
-
dynamicParams: [],
|
|
184
|
-
routeType: 'sitemap',
|
|
185
|
-
...readRouteFileContent(relFile)
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
else if (/^robots\.(ts|js|txt)$/.test(entry.name)) {
|
|
189
|
-
const relFile = path.relative(resolvedDir, entryPath).replace(/\\/g, '/');
|
|
190
|
-
discovered.push({
|
|
191
|
-
routePath: '/robots.txt',
|
|
192
|
-
sourceFilePath: relFile,
|
|
193
|
-
isDynamic: false,
|
|
194
|
-
dynamicParams: [],
|
|
195
|
-
routeType: 'robots',
|
|
196
|
-
...readRouteFileContent(relFile)
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
scanAppDir(fullRoutesDir, '/');
|
|
203
|
-
}
|
|
204
|
-
// Astro Scanner
|
|
205
|
-
else if (info.framework === 'astro') {
|
|
206
|
-
const scanAstroDir = (currentDir, routePrefix) => {
|
|
207
|
-
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
208
|
-
for (const entry of entries) {
|
|
209
|
-
const entryPath = path.join(currentDir, entry.name);
|
|
210
|
-
if (entry.isDirectory()) {
|
|
211
|
-
const nextPrefix = routePrefix === '/' ? `/${entry.name}` : `${routePrefix}/${entry.name}`;
|
|
212
|
-
scanAstroDir(entryPath, nextPrefix);
|
|
213
|
-
}
|
|
214
|
-
else if (entry.isFile() && (entry.name.endsWith('.astro') || entry.name.endsWith('.md') || entry.name.endsWith('.mdx'))) {
|
|
215
|
-
const baseName = entry.name.replace(/\.(astro|md|mdx)$/, '');
|
|
216
|
-
let routePath = routePrefix;
|
|
217
|
-
if (baseName !== 'index') {
|
|
218
|
-
routePath = routePrefix === '/' ? `/${baseName}` : `${routePrefix}/${baseName}`;
|
|
219
|
-
}
|
|
220
|
-
if (routePath === '')
|
|
221
|
-
routePath = '/';
|
|
222
|
-
const relFile = path.relative(resolvedDir, entryPath).replace(/\\/g, '/');
|
|
223
|
-
const dynamicParams = extractDynamicParams(routePath);
|
|
224
|
-
const meta = readRouteFileContent(relFile);
|
|
225
|
-
discovered.push({
|
|
226
|
-
routePath,
|
|
227
|
-
sourceFilePath: relFile,
|
|
228
|
-
isDynamic: dynamicParams.length > 0,
|
|
229
|
-
dynamicParams,
|
|
230
|
-
routeType: 'page',
|
|
231
|
-
...meta
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
scanAstroDir(fullRoutesDir, '/');
|
|
237
|
-
}
|
|
238
|
-
// Pages Router / Nuxt / SvelteKit / Static
|
|
239
|
-
else {
|
|
240
|
-
const scanGenericDir = (currentDir, routePrefix) => {
|
|
241
|
-
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
242
|
-
for (const entry of entries) {
|
|
243
|
-
const entryPath = path.join(currentDir, entry.name);
|
|
244
|
-
if (entry.isDirectory()) {
|
|
245
|
-
if (['node_modules', '.git', '.next', 'dist', 'build'].includes(entry.name))
|
|
246
|
-
continue;
|
|
247
|
-
const nextPrefix = routePrefix === '/' ? `/${entry.name}` : `${routePrefix}/${entry.name}`;
|
|
248
|
-
scanGenericDir(entryPath, nextPrefix);
|
|
249
|
-
}
|
|
250
|
-
else if (entry.isFile() && /\.(tsx|jsx|js|ts|vue|svelte|html)$/.test(entry.name)) {
|
|
251
|
-
const baseName = entry.name.replace(/\.(tsx|jsx|js|ts|vue|svelte|html)$/, '');
|
|
252
|
-
let routePath = routePrefix;
|
|
253
|
-
if (baseName !== 'index' && baseName !== 'page') {
|
|
254
|
-
routePath = routePrefix === '/' ? `/${baseName}` : `${routePrefix}/${baseName}`;
|
|
255
|
-
}
|
|
256
|
-
if (routePath === '')
|
|
257
|
-
routePath = '/';
|
|
258
|
-
const relFile = path.relative(resolvedDir, entryPath).replace(/\\/g, '/');
|
|
259
|
-
const dynamicParams = extractDynamicParams(routePath);
|
|
260
|
-
const meta = readRouteFileContent(relFile);
|
|
261
|
-
discovered.push({
|
|
262
|
-
routePath,
|
|
263
|
-
sourceFilePath: relFile,
|
|
264
|
-
isDynamic: dynamicParams.length > 0,
|
|
265
|
-
dynamicParams,
|
|
266
|
-
routeType: 'page',
|
|
267
|
-
...meta
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
};
|
|
272
|
-
scanGenericDir(fullRoutesDir, '/');
|
|
273
|
-
}
|
|
274
|
-
return discovered;
|
|
9
|
+
const adapter = defaultAdapterRegistry.getAdapterForProject(resolvedDir);
|
|
10
|
+
return adapter.getProjectInfo(resolvedDir);
|
|
275
11
|
}
|
|
276
|
-
function
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
for (const m of matches) {
|
|
281
|
-
params.push(m.replace(/[\[\].]/g, ''));
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
return params;
|
|
12
|
+
export function discoverRoutes(projectDir, _frameworkInfo) {
|
|
13
|
+
const resolvedDir = path.resolve(projectDir);
|
|
14
|
+
const adapter = defaultAdapterRegistry.getAdapterForProject(resolvedDir);
|
|
15
|
+
return adapter.discoverRoutes(resolvedDir);
|
|
285
16
|
}
|
|
286
|
-
export function mapUrlToRouteSource(targetUrl, discoveredRoutes) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
urlPath = new URL(targetUrl).pathname;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
catch {
|
|
294
|
-
urlPath = targetUrl;
|
|
295
|
-
}
|
|
296
|
-
urlPath = urlPath.replace(/\/$/, '') || '/';
|
|
297
|
-
// 1. Exact Match
|
|
298
|
-
for (const r of discoveredRoutes) {
|
|
299
|
-
const cleanRoute = r.routePath.replace(/\/$/, '') || '/';
|
|
300
|
-
if (cleanRoute === urlPath) {
|
|
301
|
-
return {
|
|
302
|
-
urlPath,
|
|
303
|
-
matchedRoute: r,
|
|
304
|
-
sourceFilePath: r.sourceFilePath,
|
|
305
|
-
confidence: 1.0,
|
|
306
|
-
resolutionMethod: 'exact_match'
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
// 2. Dynamic Pattern Match (e.g. /blog/my-post -> /blog/[slug])
|
|
311
|
-
for (const r of discoveredRoutes) {
|
|
312
|
-
if (!r.isDynamic)
|
|
313
|
-
continue;
|
|
314
|
-
const regexStr = '^' + r.routePath
|
|
315
|
-
.replace(/\[\.\.\.([a-zA-Z0-9_-]+)\]/g, '.*')
|
|
316
|
-
.replace(/\[([a-zA-Z0-9_-]+)\]/g, '[^/]+') + '$';
|
|
317
|
-
try {
|
|
318
|
-
const reg = new RegExp(regexStr);
|
|
319
|
-
if (reg.test(urlPath)) {
|
|
320
|
-
return {
|
|
321
|
-
urlPath,
|
|
322
|
-
matchedRoute: r,
|
|
323
|
-
sourceFilePath: r.sourceFilePath,
|
|
324
|
-
confidence: 0.9,
|
|
325
|
-
resolutionMethod: 'pattern_match'
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
catch {
|
|
330
|
-
// Ignored
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
// 3. Fallback Heuristic
|
|
334
|
-
const segments = urlPath.split('/').filter(Boolean);
|
|
335
|
-
if (segments.length > 0) {
|
|
336
|
-
const parentSegment = `/${segments[0]}`;
|
|
337
|
-
const parentRoute = discoveredRoutes.find(r => r.routePath.startsWith(parentSegment));
|
|
338
|
-
if (parentRoute) {
|
|
339
|
-
return {
|
|
340
|
-
urlPath,
|
|
341
|
-
matchedRoute: parentRoute,
|
|
342
|
-
sourceFilePath: parentRoute.sourceFilePath,
|
|
343
|
-
confidence: 0.6,
|
|
344
|
-
resolutionMethod: 'fallback_heuristic'
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
return {
|
|
349
|
-
urlPath,
|
|
350
|
-
confidence: 0.0,
|
|
351
|
-
resolutionMethod: 'unmapped'
|
|
352
|
-
};
|
|
17
|
+
export function mapUrlToRouteSource(targetUrl, discoveredRoutes, projectDir = '.') {
|
|
18
|
+
const resolvedDir = path.resolve(projectDir);
|
|
19
|
+
const adapter = defaultAdapterRegistry.getAdapterForProject(resolvedDir);
|
|
20
|
+
return adapter.mapRouteToSource(targetUrl, discoveredRoutes);
|
|
353
21
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Finding } from '../types/findings.js';
|
|
2
|
+
export interface SarifPhysicalLocation {
|
|
3
|
+
artifactLocation: {
|
|
4
|
+
uri: string;
|
|
5
|
+
uriBaseId?: string;
|
|
6
|
+
};
|
|
7
|
+
region?: {
|
|
8
|
+
startLine: number;
|
|
9
|
+
startColumn?: number;
|
|
10
|
+
endLine?: number;
|
|
11
|
+
endColumn?: number;
|
|
12
|
+
snippet?: {
|
|
13
|
+
text: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export interface SarifResult {
|
|
18
|
+
ruleId: string;
|
|
19
|
+
ruleIndex?: number;
|
|
20
|
+
level: 'error' | 'warning' | 'note' | 'none';
|
|
21
|
+
message: {
|
|
22
|
+
text: string;
|
|
23
|
+
};
|
|
24
|
+
locations?: Array<{
|
|
25
|
+
physicalLocation: SarifPhysicalLocation;
|
|
26
|
+
}>;
|
|
27
|
+
fixes?: Array<{
|
|
28
|
+
description: {
|
|
29
|
+
text: string;
|
|
30
|
+
};
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
export interface SarifRule {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
shortDescription: {
|
|
37
|
+
text: string;
|
|
38
|
+
};
|
|
39
|
+
fullDescription?: {
|
|
40
|
+
text: string;
|
|
41
|
+
};
|
|
42
|
+
help?: {
|
|
43
|
+
text: string;
|
|
44
|
+
};
|
|
45
|
+
defaultConfiguration: {
|
|
46
|
+
level: 'error' | 'warning' | 'note';
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export interface SarifReport {
|
|
50
|
+
$schema: string;
|
|
51
|
+
version: string;
|
|
52
|
+
runs: Array<{
|
|
53
|
+
tool: {
|
|
54
|
+
driver: {
|
|
55
|
+
name: string;
|
|
56
|
+
version: string;
|
|
57
|
+
informationUri: string;
|
|
58
|
+
rules: SarifRule[];
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
results: SarifResult[];
|
|
62
|
+
}>;
|
|
63
|
+
}
|
|
64
|
+
export declare function exportFindingsToSarif(findings: Finding[], projectRootDir?: string, toolVersion?: string): SarifReport;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export function exportFindingsToSarif(findings, projectRootDir = '.', toolVersion = '1.2.0') {
|
|
2
|
+
const ruleMap = new Map();
|
|
3
|
+
const results = [];
|
|
4
|
+
const mapSeverityToSarifLevel = (sev) => {
|
|
5
|
+
switch (sev) {
|
|
6
|
+
case 'critical':
|
|
7
|
+
case 'high':
|
|
8
|
+
return 'error';
|
|
9
|
+
case 'medium':
|
|
10
|
+
return 'warning';
|
|
11
|
+
case 'low':
|
|
12
|
+
case 'info':
|
|
13
|
+
default:
|
|
14
|
+
return 'note';
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
for (const f of findings) {
|
|
18
|
+
if (!ruleMap.has(f.id)) {
|
|
19
|
+
ruleMap.set(f.id, {
|
|
20
|
+
id: f.id,
|
|
21
|
+
name: f.title.replace(/[^a-zA-Z0-9]/g, '_'),
|
|
22
|
+
shortDescription: { text: f.title },
|
|
23
|
+
fullDescription: { text: f.evidence },
|
|
24
|
+
help: { text: `${f.recommendation} (Expected Impact: ${f.expectedImpact})` },
|
|
25
|
+
defaultConfiguration: {
|
|
26
|
+
level: mapSeverityToSarifLevel(f.severity)
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const relPath = f.sourceLocation?.filePath || (f.affectedUrl.startsWith('/') ? f.affectedUrl.slice(1) : f.affectedUrl);
|
|
31
|
+
const startLine = f.sourceRange?.startLine || f.sourceLocation?.startLine || f.sourceLocation?.line || 1;
|
|
32
|
+
const endLine = f.sourceRange?.endLine || f.sourceLocation?.endLine || startLine;
|
|
33
|
+
results.push({
|
|
34
|
+
ruleId: f.id,
|
|
35
|
+
level: mapSeverityToSarifLevel(f.severity),
|
|
36
|
+
message: {
|
|
37
|
+
text: `${f.title}: ${f.evidence}. ${f.recommendation}`
|
|
38
|
+
},
|
|
39
|
+
locations: [
|
|
40
|
+
{
|
|
41
|
+
physicalLocation: {
|
|
42
|
+
artifactLocation: {
|
|
43
|
+
uri: relPath.replace(/\\/g, '/')
|
|
44
|
+
},
|
|
45
|
+
region: {
|
|
46
|
+
startLine,
|
|
47
|
+
endLine,
|
|
48
|
+
snippet: f.sourceRange?.snippet ? { text: f.sourceRange.snippet } : undefined
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
fixes: f.suggestedFix?.snippet
|
|
54
|
+
? [
|
|
55
|
+
{
|
|
56
|
+
description: {
|
|
57
|
+
text: `${f.suggestedFix.explanation}\n\`\`\`\n${f.suggestedFix.snippet}\n\`\`\``
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
: undefined
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
|
|
66
|
+
version: '2.1.0',
|
|
67
|
+
runs: [
|
|
68
|
+
{
|
|
69
|
+
tool: {
|
|
70
|
+
driver: {
|
|
71
|
+
name: 'SEO Gravity',
|
|
72
|
+
version: toolVersion,
|
|
73
|
+
informationUri: 'https://github.com/thedevbob005/seo-gravity-mcp',
|
|
74
|
+
rules: Array.from(ruleMap.values())
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
results
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { ProjectSnapshot, RegressionReport } from '../types/findings.js';
|
|
1
|
+
import { ProjectSnapshot, RegressionReport, GitMetadata } from '../types/findings.js';
|
|
2
2
|
export interface CreateSnapshotOptions {
|
|
3
3
|
baseUrl?: string;
|
|
4
4
|
maxCrawlDepth?: number;
|
|
5
5
|
maxPagesToAudit?: number;
|
|
6
6
|
includeCrawlGraph?: boolean;
|
|
7
7
|
}
|
|
8
|
+
export declare function computeLogicalPageId(urlOrPath: string): string;
|
|
9
|
+
export declare function extractGitMetadata(projectDir: string): GitMetadata;
|
|
8
10
|
export declare function createProjectSnapshot(projectPath: string, options?: CreateSnapshotOptions): Promise<ProjectSnapshot>;
|
|
9
11
|
export declare function compareSnapshots(baseline: ProjectSnapshot, current: ProjectSnapshot): RegressionReport;
|