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.
Files changed (67) hide show
  1. package/README.md +118 -105
  2. package/dist/adapters/adapterRegistry.d.ts +7 -0
  3. package/dist/adapters/adapterRegistry.js +30 -0
  4. package/dist/adapters/astroAdapter.d.ts +17 -0
  5. package/dist/adapters/astroAdapter.js +191 -0
  6. package/dist/adapters/nextAppAdapter.d.ts +17 -0
  7. package/dist/adapters/nextAppAdapter.js +265 -0
  8. package/dist/adapters/nextPagesAdapter.d.ts +17 -0
  9. package/dist/adapters/nextPagesAdapter.js +209 -0
  10. package/dist/adapters/remixAdapter.d.ts +16 -0
  11. package/dist/adapters/remixAdapter.js +118 -0
  12. package/dist/adapters/staticAdapter.d.ts +16 -0
  13. package/dist/adapters/staticAdapter.js +117 -0
  14. package/dist/adapters/svelteKitAdapter.d.ts +16 -0
  15. package/dist/adapters/svelteKitAdapter.js +122 -0
  16. package/dist/adapters/types.d.ts +39 -0
  17. package/dist/adapters/types.js +1 -0
  18. package/dist/adapters/viteReactAdapter.d.ts +16 -0
  19. package/dist/adapters/viteReactAdapter.js +189 -0
  20. package/dist/benchmark/correlationBenchmark.d.ts +1 -0
  21. package/dist/benchmark/correlationBenchmark.js +96 -0
  22. package/dist/benchmark/invariantBenchmark.d.ts +1 -0
  23. package/dist/benchmark/invariantBenchmark.js +47 -0
  24. package/dist/cli.d.ts +8 -0
  25. package/dist/cli.js +162 -0
  26. package/dist/index.d.ts +3 -1
  27. package/dist/index.js +8 -646
  28. package/dist/invariants/registry.d.ts +12 -0
  29. package/dist/invariants/registry.js +178 -0
  30. package/dist/invariants/types.d.ts +38 -0
  31. package/dist/invariants/types.js +1 -0
  32. package/dist/providers/providerRegistry.d.ts +65 -0
  33. package/dist/providers/providerRegistry.js +64 -0
  34. package/dist/providers/types.d.ts +22 -0
  35. package/dist/providers/types.js +1 -0
  36. package/dist/server/registry.d.ts +3 -0
  37. package/dist/server/registry.js +559 -0
  38. package/dist/server/server.d.ts +70 -0
  39. package/dist/server/server.js +53 -0
  40. package/dist/server/types.d.ts +7 -0
  41. package/dist/server/types.js +1 -0
  42. package/dist/test.js +108 -138
  43. package/dist/tools/orchestration.js +23 -21
  44. package/dist/types/canonical.d.ts +64 -0
  45. package/dist/types/canonical.js +1 -0
  46. package/dist/types/findings.d.ts +22 -0
  47. package/dist/types/findings.js +1 -1
  48. package/dist/types/seo.d.ts +24 -0
  49. package/dist/utils/astLocator.d.ts +14 -0
  50. package/dist/utils/astLocator.js +137 -0
  51. package/dist/utils/cacheManager.d.ts +29 -0
  52. package/dist/utils/cacheManager.js +60 -0
  53. package/dist/utils/experimentEngine.d.ts +26 -0
  54. package/dist/utils/experimentEngine.js +41 -0
  55. package/dist/utils/findingEngine.d.ts +2 -1
  56. package/dist/utils/findingEngine.js +1 -0
  57. package/dist/utils/gitDiffEngine.d.ts +28 -0
  58. package/dist/utils/gitDiffEngine.js +109 -0
  59. package/dist/utils/opportunityEngine.d.ts +19 -0
  60. package/dist/utils/opportunityEngine.js +61 -0
  61. package/dist/utils/projectScanner.d.ts +4 -2
  62. package/dist/utils/projectScanner.js +14 -346
  63. package/dist/utils/sarifExporter.d.ts +64 -0
  64. package/dist/utils/sarifExporter.js +81 -0
  65. package/dist/utils/snapshotEngine.d.ts +3 -1
  66. package/dist/utils/snapshotEngine.js +232 -94
  67. package/package.json +8 -5
@@ -0,0 +1,265 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { inspectSourceFileAST } from '../utils/astLocator.js';
4
+ export class NextAppAdapter {
5
+ id = 'nextjs-app-router';
6
+ name = 'Next.js App Router';
7
+ detect(projectDir) {
8
+ const hasPkg = fs.existsSync(path.join(projectDir, 'package.json'));
9
+ if (!hasPkg)
10
+ return false;
11
+ try {
12
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf-8'));
13
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
14
+ if (!deps['next'])
15
+ return false;
16
+ return ((fs.existsSync(path.join(projectDir, 'app')) && fs.statSync(path.join(projectDir, 'app')).isDirectory()) ||
17
+ (fs.existsSync(path.join(projectDir, 'src/app')) && fs.statSync(path.join(projectDir, 'src/app')).isDirectory()));
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ getProjectInfo(projectDir) {
24
+ const pkgPath = path.join(projectDir, 'package.json');
25
+ let pkg = {};
26
+ if (fs.existsSync(pkgPath)) {
27
+ try {
28
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
29
+ }
30
+ catch { }
31
+ }
32
+ const hasSrcApp = fs.existsSync(path.join(projectDir, 'src/app'));
33
+ const routesDir = hasSrcApp ? 'src/app' : 'app';
34
+ return {
35
+ framework: 'nextjs-app-router',
36
+ name: pkg.name || path.basename(projectDir),
37
+ version: pkg.dependencies?.next || pkg.devDependencies?.next,
38
+ packageManager: fs.existsSync(path.join(projectDir, 'pnpm-lock.yaml'))
39
+ ? 'pnpm'
40
+ : fs.existsSync(path.join(projectDir, 'yarn.lock'))
41
+ ? 'yarn'
42
+ : 'npm',
43
+ hasTypeScript: fs.existsSync(path.join(projectDir, 'tsconfig.json')),
44
+ hasSitemapConfig: Boolean(this.findSitemapConfig(projectDir)),
45
+ hasRobotsConfig: Boolean(this.findRobotsConfig(projectDir)),
46
+ hasLlmsTxt: Boolean(this.findLlmsTxt(projectDir)),
47
+ rootDir: path.resolve(projectDir),
48
+ routesDir,
49
+ devCommand: 'npm run dev',
50
+ defaultDevPort: 3000
51
+ };
52
+ }
53
+ discoverRoutes(projectDir) {
54
+ const info = this.getProjectInfo(projectDir);
55
+ const fullRoutesDir = path.join(projectDir, info.routesDir || 'app');
56
+ const discovered = [];
57
+ if (!fs.existsSync(fullRoutesDir))
58
+ return discovered;
59
+ const scanDir = (currentDir, routePrefix) => {
60
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
61
+ for (const entry of entries) {
62
+ const entryPath = path.join(currentDir, entry.name);
63
+ if (entry.isDirectory()) {
64
+ const segment = entry.name.startsWith('(') && entry.name.endsWith(')') ? '' : entry.name;
65
+ const nextPrefix = segment ? (routePrefix === '/' ? `/${segment}` : `${routePrefix}/${segment}`) : routePrefix;
66
+ scanDir(entryPath, nextPrefix);
67
+ }
68
+ else if (entry.isFile()) {
69
+ if (/^page\.(tsx|jsx|js|ts)$/.test(entry.name)) {
70
+ const relFile = path.relative(projectDir, entryPath).replace(/\\/g, '/');
71
+ const routePath = routePrefix === '' ? '/' : routePrefix;
72
+ const dynamicParams = this.extractDynamicParams(routePath);
73
+ const astInfo = inspectSourceFileAST(entryPath);
74
+ discovered.push({
75
+ routePath,
76
+ sourceFilePath: relFile,
77
+ isDynamic: dynamicParams.length > 0,
78
+ dynamicParams,
79
+ routeType: 'page',
80
+ hasMetadataExport: astInfo.hasMetadataExport,
81
+ hasGenerateMetadata: astInfo.hasGenerateMetadata,
82
+ hasSchemaMarkup: astInfo.hasSchemaMarkup,
83
+ hasHeadComponent: false,
84
+ isClientComponent: /['"]use client['"]/.test(fs.readFileSync(entryPath, 'utf-8')),
85
+ astSourceRange: astInfo.metadataRange
86
+ });
87
+ }
88
+ else if (/^sitemap\.(ts|js|xml)$/.test(entry.name)) {
89
+ const relFile = path.relative(projectDir, entryPath).replace(/\\/g, '/');
90
+ discovered.push({
91
+ routePath: '/sitemap.xml',
92
+ sourceFilePath: relFile,
93
+ isDynamic: false,
94
+ dynamicParams: [],
95
+ routeType: 'sitemap',
96
+ hasMetadataExport: false,
97
+ hasGenerateMetadata: false,
98
+ hasSchemaMarkup: false,
99
+ hasHeadComponent: false,
100
+ isClientComponent: false
101
+ });
102
+ }
103
+ else if (/^robots\.(ts|js|txt)$/.test(entry.name)) {
104
+ const relFile = path.relative(projectDir, entryPath).replace(/\\/g, '/');
105
+ discovered.push({
106
+ routePath: '/robots.txt',
107
+ sourceFilePath: relFile,
108
+ isDynamic: false,
109
+ dynamicParams: [],
110
+ routeType: 'robots',
111
+ hasMetadataExport: false,
112
+ hasGenerateMetadata: false,
113
+ hasSchemaMarkup: false,
114
+ hasHeadComponent: false,
115
+ isClientComponent: false
116
+ });
117
+ }
118
+ }
119
+ }
120
+ };
121
+ scanDir(fullRoutesDir, '/');
122
+ return discovered;
123
+ }
124
+ mapRouteToSource(targetUrl, routes) {
125
+ let urlPath = targetUrl;
126
+ try {
127
+ if (targetUrl.startsWith('http')) {
128
+ urlPath = new URL(targetUrl).pathname;
129
+ }
130
+ }
131
+ catch {
132
+ urlPath = targetUrl;
133
+ }
134
+ urlPath = urlPath.replace(/\/$/, '') || '/';
135
+ for (const r of routes) {
136
+ const clean = r.routePath.replace(/\/$/, '') || '/';
137
+ if (clean === urlPath) {
138
+ return {
139
+ urlPath,
140
+ matchedRoute: r,
141
+ sourceFilePath: r.sourceFilePath,
142
+ sourceRange: r.astSourceRange,
143
+ confidence: 1.0,
144
+ resolutionMethod: 'exact_match'
145
+ };
146
+ }
147
+ }
148
+ for (const r of routes) {
149
+ if (!r.isDynamic)
150
+ continue;
151
+ const regexStr = '^' + r.routePath
152
+ .replace(/\[\.\.\.([a-zA-Z0-9_-]+)\]/g, '.*')
153
+ .replace(/\[([a-zA-Z0-9_-]+)\]/g, '[^/]+') + '$';
154
+ try {
155
+ if (new RegExp(regexStr).test(urlPath)) {
156
+ return {
157
+ urlPath,
158
+ matchedRoute: r,
159
+ sourceFilePath: r.sourceFilePath,
160
+ sourceRange: r.astSourceRange,
161
+ confidence: 0.95,
162
+ resolutionMethod: 'pattern_match'
163
+ };
164
+ }
165
+ }
166
+ catch { }
167
+ }
168
+ return {
169
+ urlPath,
170
+ confidence: 0.0,
171
+ resolutionMethod: 'unmapped'
172
+ };
173
+ }
174
+ async findMetadataImplementation(projectDir, route) {
175
+ const fullPath = path.join(projectDir, route.sourceFilePath);
176
+ const ast = inspectSourceFileAST(fullPath);
177
+ if (ast.hasMetadataExport) {
178
+ return {
179
+ hasMetadata: true,
180
+ type: 'static_export',
181
+ sourceRange: ast.metadataRange,
182
+ titleSnippet: ast.extractedTitle,
183
+ descriptionSnippet: ast.extractedDescription
184
+ };
185
+ }
186
+ if (ast.hasGenerateMetadata) {
187
+ return {
188
+ hasMetadata: true,
189
+ type: 'dynamic_function',
190
+ sourceRange: ast.metadataRange
191
+ };
192
+ }
193
+ return { hasMetadata: false, type: 'none' };
194
+ }
195
+ async findCanonicalDeclaration(projectDir, route) {
196
+ const fullPath = path.join(projectDir, route.sourceFilePath);
197
+ const ast = inspectSourceFileAST(fullPath);
198
+ if (ast.hasCanonicalDeclaration) {
199
+ return {
200
+ hasCanonical: true,
201
+ type: 'metadata_property',
202
+ canonicalUrl: ast.extractedCanonical,
203
+ sourceRange: ast.canonicalRange
204
+ };
205
+ }
206
+ return { hasCanonical: false, type: 'none' };
207
+ }
208
+ async findSchemaDeclaration(projectDir, route) {
209
+ const fullPath = path.join(projectDir, route.sourceFilePath);
210
+ const ast = inspectSourceFileAST(fullPath);
211
+ return {
212
+ hasSchema: ast.hasSchemaMarkup,
213
+ typesFound: ast.hasSchemaMarkup ? ['JSON-LD'] : [],
214
+ sourceRange: ast.schemaRange
215
+ };
216
+ }
217
+ findRobotsConfig(projectDir) {
218
+ const paths = [
219
+ 'app/robots.ts',
220
+ 'app/robots.js',
221
+ 'src/app/robots.ts',
222
+ 'src/app/robots.js',
223
+ 'public/robots.txt',
224
+ 'robots.txt'
225
+ ];
226
+ for (const p of paths) {
227
+ if (fs.existsSync(path.join(projectDir, p)))
228
+ return p;
229
+ }
230
+ return null;
231
+ }
232
+ findSitemapConfig(projectDir) {
233
+ const paths = [
234
+ 'app/sitemap.ts',
235
+ 'app/sitemap.js',
236
+ 'src/app/sitemap.ts',
237
+ 'src/app/sitemap.js',
238
+ 'public/sitemap.xml',
239
+ 'sitemap.xml'
240
+ ];
241
+ for (const p of paths) {
242
+ if (fs.existsSync(path.join(projectDir, p)))
243
+ return p;
244
+ }
245
+ return null;
246
+ }
247
+ findLlmsTxt(projectDir) {
248
+ const paths = ['public/llms.txt', 'llms.txt', 'public/llms-full.txt', 'llms-full.txt'];
249
+ for (const p of paths) {
250
+ if (fs.existsSync(path.join(projectDir, p)))
251
+ return p;
252
+ }
253
+ return null;
254
+ }
255
+ extractDynamicParams(routePath) {
256
+ const params = [];
257
+ const matches = routePath.match(/\[\.{0,3}([a-zA-Z0-9_-]+)\]/g);
258
+ if (matches) {
259
+ for (const m of matches) {
260
+ params.push(m.replace(/[\[\].]/g, ''));
261
+ }
262
+ }
263
+ return params;
264
+ }
265
+ }
@@ -0,0 +1,17 @@
1
+ import { FrameworkAdapter, MetadataLocationInfo, CanonicalLocationInfo, SchemaLocationInfo } from './types.js';
2
+ import { DiscoveredRoute, ProjectFrameworkInfo, RouteSourceMapping } from '../types/findings.js';
3
+ export declare class NextPagesAdapter implements FrameworkAdapter {
4
+ id: string;
5
+ name: string;
6
+ detect(projectDir: string): boolean;
7
+ getProjectInfo(projectDir: string): ProjectFrameworkInfo;
8
+ discoverRoutes(projectDir: string): DiscoveredRoute[];
9
+ mapRouteToSource(targetUrl: string, routes: DiscoveredRoute[]): RouteSourceMapping;
10
+ findMetadataImplementation(projectDir: string, route: DiscoveredRoute): Promise<MetadataLocationInfo>;
11
+ findCanonicalDeclaration(projectDir: string, route: DiscoveredRoute): Promise<CanonicalLocationInfo>;
12
+ findSchemaDeclaration(projectDir: string, route: DiscoveredRoute): Promise<SchemaLocationInfo>;
13
+ findRobotsConfig(projectDir: string): string | null;
14
+ findSitemapConfig(projectDir: string): string | null;
15
+ findLlmsTxt(projectDir: string): string | null;
16
+ private extractDynamicParams;
17
+ }
@@ -0,0 +1,209 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ export class NextPagesAdapter {
4
+ id = 'nextjs-pages-router';
5
+ name = 'Next.js Pages Router';
6
+ detect(projectDir) {
7
+ const hasPkg = fs.existsSync(path.join(projectDir, 'package.json'));
8
+ if (!hasPkg)
9
+ return false;
10
+ try {
11
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf-8'));
12
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
13
+ if (!deps['next'])
14
+ return false;
15
+ const hasPages = fs.existsSync(path.join(projectDir, 'pages')) || fs.existsSync(path.join(projectDir, 'src/pages'));
16
+ const hasApp = fs.existsSync(path.join(projectDir, 'app')) || fs.existsSync(path.join(projectDir, 'src/app'));
17
+ return hasPages && !hasApp;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ getProjectInfo(projectDir) {
24
+ const pkgPath = path.join(projectDir, 'package.json');
25
+ let pkg = {};
26
+ if (fs.existsSync(pkgPath)) {
27
+ try {
28
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
29
+ }
30
+ catch { }
31
+ }
32
+ const hasSrcPages = fs.existsSync(path.join(projectDir, 'src/pages'));
33
+ const routesDir = hasSrcPages ? 'src/pages' : 'pages';
34
+ return {
35
+ framework: 'nextjs-pages-router',
36
+ name: pkg.name || path.basename(projectDir),
37
+ version: pkg.dependencies?.next || pkg.devDependencies?.next,
38
+ packageManager: 'npm',
39
+ hasTypeScript: fs.existsSync(path.join(projectDir, 'tsconfig.json')),
40
+ hasSitemapConfig: Boolean(this.findSitemapConfig(projectDir)),
41
+ hasRobotsConfig: Boolean(this.findRobotsConfig(projectDir)),
42
+ hasLlmsTxt: Boolean(this.findLlmsTxt(projectDir)),
43
+ rootDir: path.resolve(projectDir),
44
+ routesDir,
45
+ devCommand: 'npm run dev',
46
+ defaultDevPort: 3000
47
+ };
48
+ }
49
+ discoverRoutes(projectDir) {
50
+ const info = this.getProjectInfo(projectDir);
51
+ const fullRoutesDir = path.join(projectDir, info.routesDir || 'pages');
52
+ const discovered = [];
53
+ if (!fs.existsSync(fullRoutesDir))
54
+ return discovered;
55
+ const scanDir = (currentDir, routePrefix) => {
56
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
57
+ for (const entry of entries) {
58
+ const entryPath = path.join(currentDir, entry.name);
59
+ if (entry.isDirectory()) {
60
+ if (entry.name === 'api')
61
+ continue;
62
+ const nextPrefix = routePrefix === '/' ? `/${entry.name}` : `${routePrefix}/${entry.name}`;
63
+ scanDir(entryPath, nextPrefix);
64
+ }
65
+ else if (entry.isFile() && /\.(tsx|jsx|js|ts)$/.test(entry.name)) {
66
+ if (entry.name.startsWith('_'))
67
+ continue;
68
+ const baseName = entry.name.replace(/\.(tsx|jsx|js|ts)$/, '');
69
+ let routePath = routePrefix;
70
+ if (baseName !== 'index') {
71
+ routePath = routePrefix === '/' ? `/${baseName}` : `${routePrefix}/${baseName}`;
72
+ }
73
+ if (routePath === '')
74
+ routePath = '/';
75
+ const relFile = path.relative(projectDir, entryPath).replace(/\\/g, '/');
76
+ const content = fs.readFileSync(entryPath, 'utf-8');
77
+ const dynamicParams = this.extractDynamicParams(routePath);
78
+ discovered.push({
79
+ routePath,
80
+ sourceFilePath: relFile,
81
+ isDynamic: dynamicParams.length > 0,
82
+ dynamicParams,
83
+ routeType: 'page',
84
+ hasMetadataExport: /<Head\b|<title\b|<NextSeo\b/i.test(content),
85
+ hasGenerateMetadata: false,
86
+ hasSchemaMarkup: /application\/ld\+json/.test(content),
87
+ hasHeadComponent: /<Head\b|<NextSeo\b/i.test(content),
88
+ isClientComponent: false
89
+ });
90
+ }
91
+ }
92
+ };
93
+ scanDir(fullRoutesDir, '/');
94
+ return discovered;
95
+ }
96
+ mapRouteToSource(targetUrl, routes) {
97
+ let urlPath = targetUrl;
98
+ try {
99
+ if (targetUrl.startsWith('http')) {
100
+ urlPath = new URL(targetUrl).pathname;
101
+ }
102
+ }
103
+ catch {
104
+ urlPath = targetUrl;
105
+ }
106
+ urlPath = urlPath.replace(/\/$/, '') || '/';
107
+ for (const r of routes) {
108
+ const clean = r.routePath.replace(/\/$/, '') || '/';
109
+ if (clean === urlPath) {
110
+ return {
111
+ urlPath,
112
+ matchedRoute: r,
113
+ sourceFilePath: r.sourceFilePath,
114
+ confidence: 1.0,
115
+ resolutionMethod: 'exact_match'
116
+ };
117
+ }
118
+ }
119
+ for (const r of routes) {
120
+ if (!r.isDynamic)
121
+ continue;
122
+ const regexStr = '^' + r.routePath
123
+ .replace(/\[\.\.\.([a-zA-Z0-9_-]+)\]/g, '.*')
124
+ .replace(/\[([a-zA-Z0-9_-]+)\]/g, '[^/]+') + '$';
125
+ try {
126
+ if (new RegExp(regexStr).test(urlPath)) {
127
+ return {
128
+ urlPath,
129
+ matchedRoute: r,
130
+ sourceFilePath: r.sourceFilePath,
131
+ confidence: 0.95,
132
+ resolutionMethod: 'pattern_match'
133
+ };
134
+ }
135
+ }
136
+ catch { }
137
+ }
138
+ return {
139
+ urlPath,
140
+ confidence: 0.0,
141
+ resolutionMethod: 'unmapped'
142
+ };
143
+ }
144
+ async findMetadataImplementation(projectDir, route) {
145
+ const fullPath = path.join(projectDir, route.sourceFilePath);
146
+ if (!fs.existsSync(fullPath))
147
+ return { hasMetadata: false, type: 'none' };
148
+ const content = fs.readFileSync(fullPath, 'utf-8');
149
+ const hasHead = /<Head\b|<NextSeo\b|<title\b/i.test(content);
150
+ return {
151
+ hasMetadata: hasHead,
152
+ type: hasHead ? 'jsx_head' : 'none'
153
+ };
154
+ }
155
+ async findCanonicalDeclaration(projectDir, route) {
156
+ const fullPath = path.join(projectDir, route.sourceFilePath);
157
+ if (!fs.existsSync(fullPath))
158
+ return { hasCanonical: false, type: 'none' };
159
+ const content = fs.readFileSync(fullPath, 'utf-8');
160
+ return {
161
+ hasCanonical: /rel=["']canonical["']/i.test(content),
162
+ type: /rel=["']canonical["']/i.test(content) ? 'link_jsx_tag' : 'none'
163
+ };
164
+ }
165
+ async findSchemaDeclaration(projectDir, route) {
166
+ const fullPath = path.join(projectDir, route.sourceFilePath);
167
+ if (!fs.existsSync(fullPath))
168
+ return { hasSchema: false, typesFound: [] };
169
+ const content = fs.readFileSync(fullPath, 'utf-8');
170
+ return {
171
+ hasSchema: /application\/ld\+json/i.test(content),
172
+ typesFound: /application\/ld\+json/i.test(content) ? ['JSON-LD'] : []
173
+ };
174
+ }
175
+ findRobotsConfig(projectDir) {
176
+ const paths = ['public/robots.txt', 'robots.txt'];
177
+ for (const p of paths) {
178
+ if (fs.existsSync(path.join(projectDir, p)))
179
+ return p;
180
+ }
181
+ return null;
182
+ }
183
+ findSitemapConfig(projectDir) {
184
+ const paths = ['public/sitemap.xml', 'sitemap.xml'];
185
+ for (const p of paths) {
186
+ if (fs.existsSync(path.join(projectDir, p)))
187
+ return p;
188
+ }
189
+ return null;
190
+ }
191
+ findLlmsTxt(projectDir) {
192
+ const paths = ['public/llms.txt', 'llms.txt'];
193
+ for (const p of paths) {
194
+ if (fs.existsSync(path.join(projectDir, p)))
195
+ return p;
196
+ }
197
+ return null;
198
+ }
199
+ extractDynamicParams(routePath) {
200
+ const params = [];
201
+ const matches = routePath.match(/\[\.{0,3}([a-zA-Z0-9_-]+)\]/g);
202
+ if (matches) {
203
+ for (const m of matches) {
204
+ params.push(m.replace(/[\[\].]/g, ''));
205
+ }
206
+ }
207
+ return params;
208
+ }
209
+ }
@@ -0,0 +1,16 @@
1
+ import { FrameworkAdapter, MetadataLocationInfo, CanonicalLocationInfo, SchemaLocationInfo } from './types.js';
2
+ import { DiscoveredRoute, ProjectFrameworkInfo, RouteSourceMapping } from '../types/findings.js';
3
+ export declare class RemixAdapter implements FrameworkAdapter {
4
+ id: string;
5
+ name: string;
6
+ detect(projectDir: string): boolean;
7
+ getProjectInfo(projectDir: string): ProjectFrameworkInfo;
8
+ discoverRoutes(projectDir: string): DiscoveredRoute[];
9
+ mapRouteToSource(targetUrl: string, routes: DiscoveredRoute[]): RouteSourceMapping;
10
+ findMetadataImplementation(projectDir: string, route: DiscoveredRoute): Promise<MetadataLocationInfo>;
11
+ findCanonicalDeclaration(projectDir: string, route: DiscoveredRoute): Promise<CanonicalLocationInfo>;
12
+ findSchemaDeclaration(projectDir: string, route: DiscoveredRoute): Promise<SchemaLocationInfo>;
13
+ findRobotsConfig(projectDir: string): string | null;
14
+ findSitemapConfig(projectDir: string): string | null;
15
+ findLlmsTxt(projectDir: string): string | null;
16
+ }
@@ -0,0 +1,118 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ export class RemixAdapter {
4
+ id = 'remix';
5
+ name = 'Remix Framework';
6
+ detect(projectDir) {
7
+ const pkgPath = path.join(projectDir, 'package.json');
8
+ if (!fs.existsSync(pkgPath))
9
+ return false;
10
+ try {
11
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
12
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
13
+ return Boolean(deps['@remix-run/react'] || deps['@remix-run/node']);
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ getProjectInfo(projectDir) {
20
+ return {
21
+ framework: 'remix',
22
+ name: path.basename(projectDir),
23
+ hasTypeScript: fs.existsSync(path.join(projectDir, 'tsconfig.json')),
24
+ hasSitemapConfig: Boolean(this.findSitemapConfig(projectDir)),
25
+ hasRobotsConfig: Boolean(this.findRobotsConfig(projectDir)),
26
+ hasLlmsTxt: Boolean(this.findLlmsTxt(projectDir)),
27
+ rootDir: path.resolve(projectDir),
28
+ routesDir: 'app/routes',
29
+ devCommand: 'npm run dev',
30
+ defaultDevPort: 3000
31
+ };
32
+ }
33
+ discoverRoutes(projectDir) {
34
+ const routesDir = path.join(projectDir, 'app/routes');
35
+ const discovered = [];
36
+ if (!fs.existsSync(routesDir))
37
+ return discovered;
38
+ const entries = fs.readdirSync(routesDir);
39
+ for (const file of entries) {
40
+ if (!/\.(tsx|jsx|js|ts)$/.test(file))
41
+ continue;
42
+ const base = file.replace(/\.(tsx|jsx|js|ts)$/, '');
43
+ const rel = path.join('app/routes', file).replace(/\\/g, '/');
44
+ const content = fs.readFileSync(path.join(routesDir, file), 'utf-8');
45
+ let routePath = '/' + base.replace(/\._index$/, '').replace(/\./g, '/').replace(/_index$/, '');
46
+ if (routePath === '//' || routePath === '')
47
+ routePath = '/';
48
+ discovered.push({
49
+ routePath,
50
+ sourceFilePath: rel,
51
+ isDynamic: routePath.includes('$') || routePath.includes(':'),
52
+ dynamicParams: (routePath.match(/\$([a-zA-Z0-9_-]+)/g) || []).map(p => p.slice(1)),
53
+ routeType: 'page',
54
+ hasMetadataExport: /export\s+const\s+meta\b|export\s+function\s+meta\b/.test(content),
55
+ hasGenerateMetadata: false,
56
+ hasSchemaMarkup: /application\/ld\+json/.test(content),
57
+ hasHeadComponent: false,
58
+ isClientComponent: false
59
+ });
60
+ }
61
+ return discovered;
62
+ }
63
+ mapRouteToSource(targetUrl, routes) {
64
+ let urlPath = targetUrl.startsWith('http') ? new URL(targetUrl).pathname : targetUrl;
65
+ urlPath = urlPath.replace(/\/$/, '') || '/';
66
+ const match = routes.find(r => r.routePath === urlPath);
67
+ return {
68
+ urlPath,
69
+ matchedRoute: match,
70
+ sourceFilePath: match?.sourceFilePath,
71
+ confidence: match ? 1.0 : 0.0,
72
+ resolutionMethod: match ? 'exact_match' : 'unmapped'
73
+ };
74
+ }
75
+ async findMetadataImplementation(projectDir, route) {
76
+ const fullPath = path.join(projectDir, route.sourceFilePath);
77
+ if (!fs.existsSync(fullPath))
78
+ return { hasMetadata: false, type: 'none' };
79
+ const content = fs.readFileSync(fullPath, 'utf-8');
80
+ const hasMeta = /export\s+(const|function)\s+meta\b/.test(content);
81
+ return { hasMetadata: hasMeta, type: hasMeta ? 'static_export' : 'none' };
82
+ }
83
+ async findCanonicalDeclaration(projectDir, route) {
84
+ const fullPath = path.join(projectDir, route.sourceFilePath);
85
+ if (!fs.existsSync(fullPath))
86
+ return { hasCanonical: false, type: 'none' };
87
+ const content = fs.readFileSync(fullPath, 'utf-8');
88
+ return { hasCanonical: /canonical/.test(content), type: 'metadata_property' };
89
+ }
90
+ async findSchemaDeclaration(projectDir, route) {
91
+ const fullPath = path.join(projectDir, route.sourceFilePath);
92
+ if (!fs.existsSync(fullPath))
93
+ return { hasSchema: false, typesFound: [] };
94
+ const content = fs.readFileSync(fullPath, 'utf-8');
95
+ return { hasSchema: /application\/ld\+json/.test(content), typesFound: ['JSON-LD'] };
96
+ }
97
+ findRobotsConfig(projectDir) {
98
+ const paths = ['public/robots.txt', 'robots.txt'];
99
+ for (const p of paths)
100
+ if (fs.existsSync(path.join(projectDir, p)))
101
+ return p;
102
+ return null;
103
+ }
104
+ findSitemapConfig(projectDir) {
105
+ const paths = ['public/sitemap.xml', 'sitemap.xml'];
106
+ for (const p of paths)
107
+ if (fs.existsSync(path.join(projectDir, p)))
108
+ return p;
109
+ return null;
110
+ }
111
+ findLlmsTxt(projectDir) {
112
+ const paths = ['public/llms.txt', 'llms.txt'];
113
+ for (const p of paths)
114
+ if (fs.existsSync(path.join(projectDir, p)))
115
+ return p;
116
+ return null;
117
+ }
118
+ }
@@ -0,0 +1,16 @@
1
+ import { FrameworkAdapter, MetadataLocationInfo, CanonicalLocationInfo, SchemaLocationInfo } from './types.js';
2
+ import { DiscoveredRoute, ProjectFrameworkInfo, RouteSourceMapping } from '../types/findings.js';
3
+ export declare class StaticAdapter implements FrameworkAdapter {
4
+ id: string;
5
+ name: string;
6
+ detect(_projectDir: string): boolean;
7
+ getProjectInfo(projectDir: string): ProjectFrameworkInfo;
8
+ discoverRoutes(projectDir: string): DiscoveredRoute[];
9
+ mapRouteToSource(targetUrl: string, routes: DiscoveredRoute[]): RouteSourceMapping;
10
+ findMetadataImplementation(projectDir: string, route: DiscoveredRoute): Promise<MetadataLocationInfo>;
11
+ findCanonicalDeclaration(projectDir: string, route: DiscoveredRoute): Promise<CanonicalLocationInfo>;
12
+ findSchemaDeclaration(projectDir: string, route: DiscoveredRoute): Promise<SchemaLocationInfo>;
13
+ findRobotsConfig(projectDir: string): string | null;
14
+ findSitemapConfig(projectDir: string): string | null;
15
+ findLlmsTxt(projectDir: string): string | null;
16
+ }