@event4u/agent-config 17.2.0 → 17.3.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/skills/install-agent-config/SKILL.md +9 -9
- package/CHANGELOG.md +174 -0
- package/dist/agent-src/skills/code-intelligence/SKILL.md +13 -12
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/check_memory.ts +1 -1
- package/dist/cli/commands/recordTriggerEval.js +1 -1
- package/dist/cli-delegate/{chunk-KR5ZIFPO.js → chunk-DHSSP7V3.js} +1 -1
- package/dist/cli-delegate/chunk-E7ACTJTJ.js +6792 -0
- package/dist/cli-delegate/{chunk-BZJ4Y3CI.js → chunk-MT2WOOCY.js} +1 -1
- package/dist/cli-delegate/{chunk-CMZEKSNN.js → chunk-ORJPBER2.js} +2 -2
- package/dist/cli-delegate/{chunk-VYZGJLCA.js → chunk-RKNT4LIK.js} +22 -1286
- package/dist/cli-delegate/cmd_conformance.js +20 -14
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_doctor_neighbours.js +2 -2
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +8 -4
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +3 -3
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/code_graph_refresh.chunk.cjs +414 -0
- package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -0
- package/dist/hooks/dispatch.js +21 -11
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/hooks/egress_engine.chunk.cjs +1 -1
- package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
- package/dist/install/install.mjs +6974 -6037
- package/dist/mcp/mcp-cloudflare-catalogue.json +1 -0
- package/dist/mcp/registry-manifest.json +3 -2
- package/dist/mcp/server.json +13 -4
- package/dist/mcp/server.mjs +5561 -2463
- package/dist/scripts/_lib/egress_rules.js +8 -2
- package/dist/scripts/condense.js +9 -9
- package/dist/server/routes/discovery.js +1 -1
- package/dist/server/routes/install.js +1 -1
- package/dist/server/routes/ping.js +1 -1
- package/dist/server/routes/schema.js +4 -4
- package/dist/server/routes/settings.js +2 -6
- package/dist/server/routes/wizard.js +6 -6
- package/dist/server/schemas/settings.js +42 -42
- package/dist/shared/settingsSurface.js +1 -1
- package/dist/shared/userMd/schema.js +1 -1
- package/dist/shared/zodJsonSchema.js +47 -0
- package/dist/ui/assets/index-B0bcIgi0.js +77 -0
- package/dist/ui/index.html +1 -1
- package/package.json +3 -3
- package/src/agent-src/templates/scripts/check_memory.ts +1 -1
- package/src/config/ci-local-parity.yml +11 -0
- package/src/config/continuity-surface.json +9 -2
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/gitignore-block.txt +6 -5
- package/src/config/hook-bundle-budget.json +16 -1
- package/src/config/pack-size-budget.json +12 -1
- package/src/config/packed-binary-manifest.json +9 -0
- package/src/scripts/_cli/cmd_settings_set.ts +3 -2
- package/src/scripts/_dispatch.bash +1 -1
- package/src/scripts/_lib/egress_rules.ts +8 -2
- package/src/scripts/_lib/listing_manifests.ts +85 -0
- package/src/scripts/_lib/neighbour_census.ts +1 -0
- package/src/scripts/_lib/release_settings_surface.ts +2 -6
- package/src/scripts/adoption_status.ts +231 -27
- package/src/scripts/build_mcp_registry_manifest.ts +23 -12
- package/src/scripts/check_dependency_floors.ts +7 -0
- package/src/scripts/check_release_manifest_content.ts +17 -2
- package/src/scripts/check_release_pr_shape.ts +5 -0
- package/src/scripts/code_graph/build.ts +80 -44
- package/src/scripts/code_graph/cli.ts +260 -16
- package/src/scripts/code_graph/conventions_default.ts +228 -0
- package/src/scripts/code_graph/extract.ts +5 -2
- package/src/scripts/code_graph/extract_more.ts +105 -1
- package/src/scripts/code_graph/loader.ts +10 -2
- package/src/scripts/code_graph/map.ts +174 -41
- package/src/scripts/code_graph/map_model.ts +53 -15
- package/src/scripts/code_graph/map_verify.ts +12 -0
- package/src/scripts/code_graph/map_viewer/viewer.css +6 -0
- package/src/scripts/code_graph/map_viewer/viewer.js +114 -13
- package/src/scripts/code_graph/passes.ts +173 -0
- package/src/scripts/code_graph/providers/angular.ts +55 -0
- package/src/scripts/code_graph/providers/aspnetcore.ts +182 -0
- package/src/scripts/code_graph/providers/catalog.ts +290 -0
- package/src/scripts/code_graph/providers/django.ts +163 -0
- package/src/scripts/code_graph/providers/express.ts +25 -0
- package/src/scripts/code_graph/providers/fastapi.ts +49 -0
- package/src/scripts/code_graph/providers/flask.ts +99 -0
- package/src/scripts/code_graph/providers/go_http.ts +121 -0
- package/src/scripts/code_graph/providers/index.ts +101 -0
- package/src/scripts/code_graph/{laravel_overlay.ts → providers/laravel.ts} +113 -7
- package/src/scripts/code_graph/providers/nestjs.ts +34 -0
- package/src/scripts/code_graph/providers/nextjs.ts +91 -0
- package/src/scripts/code_graph/providers/nuxt.ts +55 -0
- package/src/scripts/code_graph/providers/react-router.ts +58 -0
- package/src/scripts/code_graph/providers/shared/detect.ts +78 -0
- package/src/scripts/code_graph/providers/shared/extractors.ts +249 -0
- package/src/scripts/code_graph/providers/shared/js.ts +252 -0
- package/src/scripts/code_graph/providers/shared/kit.ts +210 -0
- package/src/scripts/code_graph/providers/shared/php.ts +64 -0
- package/src/scripts/code_graph/providers/shared/python.ts +223 -0
- package/src/scripts/code_graph/providers/spring.ts +138 -0
- package/src/scripts/code_graph/providers/sqlalchemy.ts +62 -0
- package/src/scripts/code_graph/providers/sveltekit.ts +59 -0
- package/src/scripts/code_graph/providers/symfony.ts +142 -0
- package/src/scripts/code_graph/providers/typeorm.ts +58 -0
- package/src/scripts/code_graph/providers/types.ts +56 -0
- package/src/scripts/code_graph/providers/vue-router.ts +21 -0
- package/src/scripts/code_graph/providers/wordpress.ts +128 -0
- package/src/scripts/code_graph/refresh_cli.ts +81 -0
- package/src/scripts/code_graph/refresh_lock.ts +193 -0
- package/src/scripts/code_graph/resolution_tiers.ts +19 -7
- package/src/scripts/code_graph/scip_reader.ts +2 -2
- package/src/scripts/code_graph/sqlite_store.ts +10 -4
- package/src/scripts/code_graph/types.ts +75 -41
- package/src/scripts/code_graph/validate.ts +6 -8
- package/src/scripts/condense.ts +9 -9
- package/src/scripts/generate_settings_reference.ts +2 -6
- package/src/scripts/hook_chunks.mjs +4 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +24 -7
- package/src/scripts/hooks/code_graph_refresh_hook.ts +160 -0
- package/src/scripts/hooks/lazy_concern.ts +4 -0
- package/src/scripts/install-hooks.sh +54 -33
- package/src/scripts/install.ts +2 -6
- package/src/scripts/lint_hook_manifest.ts +2 -0
- package/src/scripts/lint_marketplace.ts +33 -0
- package/src/scripts/lint_mcp_registry_manifest.ts +36 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +1 -0
- package/src/scripts/mcp_server/graph_tools.ts +5 -1
- package/src/scripts/release.ts +2 -5
- package/src/scripts/release_drill.ts +4 -1
- package/src/scripts/release_env.ts +17 -0
- package/src/scripts/release_mix_taxonomy.json +3 -0
- package/src/server/schemas/settings.ts +42 -42
- package/src/shared/settingsSurface.ts +1 -1
- package/src/shared/userMd/schema.ts +1 -1
- package/src/shared/zodJsonSchema.ts +41 -0
- package/src/vendor/grammars/README.md +6 -5
- package/src/vendor/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/dist/cli-delegate/chunk-XIAPY44N.js +0 -4587
- package/dist/ui/assets/index-w70Ado09.js +0 -23
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Go HTTP routers: a registration on a router value → the handler function.
|
|
3
|
+
*
|
|
4
|
+
* http.HandleFunc("/x", h) · mux.HandleFunc("GET /x", h) · mux.Handle("/x", h) net/http (1.22 verb patterns)
|
|
5
|
+
* r.GET("/x", h) · g := r.Group("/api"); g.POST("/y", h) Gin (group prefix)
|
|
6
|
+
* e.GET("/x", h) Echo
|
|
7
|
+
* r.Get("/x", h) chi
|
|
8
|
+
*
|
|
9
|
+
* A receiver must be a router the same function states: assigned from a router
|
|
10
|
+
* constructor or a `.Group("/p")` of one, or a parameter of a router type; the
|
|
11
|
+
* `http` package itself when the file imports `net/http`. So `cache.Get("/x")`
|
|
12
|
+
* is no route. A handler resolves as the file names it: a function of the same
|
|
13
|
+
* file, then of the same directory (the Go package); `pkg.F` through the
|
|
14
|
+
* file's import of an in-repo package directory; `v.m` to the one method named
|
|
15
|
+
* `m` in the package. Anything else — a closure, `http.HandlerFunc(f)` — gets
|
|
16
|
+
* a route with no target. chi's `r.Route("/p", func…)` nesting is not followed.
|
|
17
|
+
*
|
|
18
|
+
* Detection is every `go.mod`: `net/http` is the standard library and declares
|
|
19
|
+
* no requirement, and a Gin, Echo or chi project carries a `go.mod` already, so
|
|
20
|
+
* the third-party modules need no marker of their own.
|
|
21
|
+
*/
|
|
22
|
+
import * as path from 'node:path';
|
|
23
|
+
|
|
24
|
+
import { goRoots } from './shared/detect.js';
|
|
25
|
+
import { joinRoute, lastArgName } from './shared/extractors.js';
|
|
26
|
+
import { type Kit, lineIndex, makeKit, packageSymbol } from './shared/kit.js';
|
|
27
|
+
import type { ConventionProvider } from './types.js';
|
|
28
|
+
|
|
29
|
+
const CONSTRUCTOR =
|
|
30
|
+
/\b(?<v>\w+)\s*:?=\s*&?(?:chi\.NewRouter|chi\.NewMux|gin\.Default|gin\.New|echo\.New|http\.NewServeMux|mux\.NewRouter)\(\)/g;
|
|
31
|
+
const GROUP = /\b(?<v>\w+)\s*:?=\s*(?<parent>\w+)\.Group\(\s*"(?<p>[^"]*)"/g;
|
|
32
|
+
const PARAM = /\b(?<v>\w+)\s+\*?(?:gin\.(?:Engine|RouterGroup|IRouter|IRoutes)|chi\.(?:Router|Mux)|http\.ServeMux|echo\.(?:Echo|Group)|mux\.Router)\b/g;
|
|
33
|
+
const REGISTER =
|
|
34
|
+
/\b(?<recv>\w+)\.(?<m>HandleFunc|Handle|GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD|Any|Get|Post|Put|Delete|Patch|Options|Head)\(\s*"(?:(?<pverb>[A-Z]+)\s+)?(?<path>\/[^"]*)"\s*,(?=(?<rest>[\s\S]{0,300}))/g;
|
|
35
|
+
const IMPORT = /^\s*(?:import\s+)?(?<alias>[\w.]+\s+)?"(?<spec>[^"]+)"\s*$/gm;
|
|
36
|
+
|
|
37
|
+
/** Local package name → import path, for the imports a file states. */
|
|
38
|
+
function imports(src: string): Map<string, string> {
|
|
39
|
+
const out = new Map<string, string>();
|
|
40
|
+
const blocks = [...src.matchAll(/^import\s*\(([\s\S]*?)^\)/gm)].map((m) => m[1] as string);
|
|
41
|
+
const singles = [...src.matchAll(/^import\s+([\w.]+\s+)?("[^"]+")\s*$/gm)].map((m) => `${m[1] ?? ''}${m[2]}`);
|
|
42
|
+
for (const text of [...blocks, ...singles]) {
|
|
43
|
+
for (const m of text.matchAll(IMPORT)) {
|
|
44
|
+
const spec = m.groups?.['spec'] as string;
|
|
45
|
+
const segs = spec.split('/');
|
|
46
|
+
const last = segs[segs.length - 1] as string;
|
|
47
|
+
const name = m.groups?.['alias']?.trim() ?? (/^v\d+$/.test(last) && segs.length > 1 ? (segs[segs.length - 2] as string) : last);
|
|
48
|
+
out.set(name, spec);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The one in-repo package directory an import path names, under a provider root. */
|
|
55
|
+
function packageDir(kit: Kit, spec: string): string | null {
|
|
56
|
+
const dirs = new Set(kit.filesWithExt('.go').map((f) => path.posix.dirname(f.path)));
|
|
57
|
+
const hits = [...dirs].filter((d) =>
|
|
58
|
+
kit.roots.some((root) => {
|
|
59
|
+
const rel = root === '' ? d : d.startsWith(`${root}/`) ? d.slice(root.length + 1) : null;
|
|
60
|
+
return rel !== null && rel !== '.' && (spec === rel || spec.endsWith(`/${rel}`));
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
return hits.length === 1 ? (hits[0] as string) : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function resolveHandler(kit: Kit, file: string, rest: string, imported: Map<string, string>): string | null {
|
|
67
|
+
const name = lastArgName(rest);
|
|
68
|
+
if (!name) return null;
|
|
69
|
+
const [head, member] = name.split('.') as [string, string | undefined];
|
|
70
|
+
if (!member) {
|
|
71
|
+
const local = kit.nodesInFile(file).find((n) => n.kind === 'function' && n.label === head);
|
|
72
|
+
return local ? local.id : packageSymbol(kit, file, head, ['function']);
|
|
73
|
+
}
|
|
74
|
+
const spec = imported.get(head);
|
|
75
|
+
if (spec) {
|
|
76
|
+
const dir = packageDir(kit, spec);
|
|
77
|
+
return dir ? packageSymbol(kit, `${dir}/_.go`, member, ['function']) : null;
|
|
78
|
+
}
|
|
79
|
+
return packageSymbol(kit, file, member, ['method']);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const goHttpProvider: ConventionProvider = {
|
|
83
|
+
id: 'go-http',
|
|
84
|
+
detect: (probe) => goRoots(probe, ['module']),
|
|
85
|
+
build: (input) => {
|
|
86
|
+
const kit = makeKit(input);
|
|
87
|
+
for (const f of kit.filesWithExt('.go')) {
|
|
88
|
+
const at = lineIndex(f.source);
|
|
89
|
+
const imported = imports(f.source);
|
|
90
|
+
const scope = (index: number): string => kit.enclosing(f.path, at(index));
|
|
91
|
+
const routers = new Map<string, string>();
|
|
92
|
+
const key = (index: number, v: string): string => `${scope(index)}\0${v}`;
|
|
93
|
+
const found = [
|
|
94
|
+
...[...f.source.matchAll(CONSTRUCTOR)].map((m) => ({ m, parent: undefined as string | undefined })),
|
|
95
|
+
...[...f.source.matchAll(PARAM)].map((m) => ({ m, parent: undefined as string | undefined })),
|
|
96
|
+
...[...f.source.matchAll(GROUP)].map((m) => ({ m, parent: m.groups?.['parent'] })),
|
|
97
|
+
].sort((a, b) => (a.m.index as number) - (b.m.index as number));
|
|
98
|
+
for (const { m, parent } of found) {
|
|
99
|
+
const v = m.groups?.['v'] as string;
|
|
100
|
+
if (parent === undefined) {
|
|
101
|
+
routers.set(key(m.index as number, v), '');
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const base = routers.get(key(m.index as number, parent));
|
|
105
|
+
if (base !== undefined) routers.set(key(m.index as number, v), joinRoute(base, m.groups?.['p']));
|
|
106
|
+
}
|
|
107
|
+
const httpPackage = [...imported].find(([, spec]) => spec === 'net/http')?.[0];
|
|
108
|
+
for (const m of f.source.matchAll(REGISTER)) {
|
|
109
|
+
const recv = m.groups?.['recv'] as string;
|
|
110
|
+
const method = m.groups?.['m'] as string;
|
|
111
|
+
const prefix = recv === httpPackage && /^Handle(Func)?$/.test(method) ? '' : routers.get(key(m.index as number, recv));
|
|
112
|
+
if (prefix === undefined) continue;
|
|
113
|
+
const verb = m.groups?.['pverb'] ?? (/^(HandleFunc|Handle|Any)$/.test(method) ? 'ANY' : method.toUpperCase());
|
|
114
|
+
const id = kit.route(f.path, verb, joinRoute(prefix, m.groups?.['path']), at(m.index as number));
|
|
115
|
+
const target = resolveHandler(kit, f.path, m.groups?.['rest'] ?? '', imported);
|
|
116
|
+
if (target) kit.emit(id, target, 'routes-to', 'route-table');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return kit.result();
|
|
120
|
+
},
|
|
121
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The convention-provider registry (ADR-298). The only module the graph core
|
|
3
|
+
* imports provider code through; adding a framework is an entry here, never a
|
|
4
|
+
* branch in the build.
|
|
5
|
+
*/
|
|
6
|
+
import type { FileRole } from '../map_model.js';
|
|
7
|
+
import type { ActiveProviderRecord } from '../types.js';
|
|
8
|
+
import { angularProvider } from './angular.js';
|
|
9
|
+
import { aspnetcoreProvider } from './aspnetcore.js';
|
|
10
|
+
import { djangoProvider } from './django.js';
|
|
11
|
+
import { expressProvider } from './express.js';
|
|
12
|
+
import { fastapiProvider } from './fastapi.js';
|
|
13
|
+
import { flaskProvider } from './flask.js';
|
|
14
|
+
import { goHttpProvider } from './go_http.js';
|
|
15
|
+
import { laravelProvider } from './laravel.js';
|
|
16
|
+
import { nestjsProvider } from './nestjs.js';
|
|
17
|
+
import { nextjsProvider } from './nextjs.js';
|
|
18
|
+
import { nuxtProvider } from './nuxt.js';
|
|
19
|
+
import { reactRouterProvider } from './react-router.js';
|
|
20
|
+
import { springProvider } from './spring.js';
|
|
21
|
+
import { sqlalchemyProvider } from './sqlalchemy.js';
|
|
22
|
+
import { sveltekitProvider } from './sveltekit.js';
|
|
23
|
+
import { symfonyProvider } from './symfony.js';
|
|
24
|
+
import { typeormProvider } from './typeorm.js';
|
|
25
|
+
import { vueRouterProvider } from './vue-router.js';
|
|
26
|
+
import { wordpressProvider } from './wordpress.js';
|
|
27
|
+
import type { ConventionProvider, ProjectProbe } from './types.js';
|
|
28
|
+
|
|
29
|
+
export type { ConventionProvider, ProjectProbe, ProviderInput, ProviderResult } from './types.js';
|
|
30
|
+
export { EMPTY_PROBE } from './types.js';
|
|
31
|
+
export { CATALOG, type CatalogRow, catalogRow, type CoverageStatus } from './catalog.js';
|
|
32
|
+
|
|
33
|
+
/** Registration order is execution order; ids are unique. */
|
|
34
|
+
export const PROVIDERS: readonly ConventionProvider[] = [
|
|
35
|
+
laravelProvider,
|
|
36
|
+
expressProvider,
|
|
37
|
+
springProvider,
|
|
38
|
+
goHttpProvider,
|
|
39
|
+
symfonyProvider,
|
|
40
|
+
wordpressProvider,
|
|
41
|
+
fastapiProvider,
|
|
42
|
+
flaskProvider,
|
|
43
|
+
djangoProvider,
|
|
44
|
+
sqlalchemyProvider,
|
|
45
|
+
reactRouterProvider,
|
|
46
|
+
nextjsProvider,
|
|
47
|
+
nestjsProvider,
|
|
48
|
+
angularProvider,
|
|
49
|
+
vueRouterProvider,
|
|
50
|
+
nuxtProvider,
|
|
51
|
+
sveltekitProvider,
|
|
52
|
+
typeormProvider,
|
|
53
|
+
aspnetcoreProvider,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/** Every marker basename a registered provider asks the probe to record. */
|
|
57
|
+
export const PROBE_MARKERS: ReadonlySet<string> = new Set(PROVIDERS.flatMap((p) => p.markers ?? []));
|
|
58
|
+
|
|
59
|
+
export interface ActiveProvider {
|
|
60
|
+
provider: ConventionProvider;
|
|
61
|
+
roots: string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The providers whose markers the probe carries, each with the package roots it applies to. */
|
|
65
|
+
export function activeProviders(probe: ProjectProbe, registry: readonly ConventionProvider[] = PROVIDERS): ActiveProvider[] {
|
|
66
|
+
const out: ActiveProvider[] = [];
|
|
67
|
+
for (const provider of registry) {
|
|
68
|
+
const roots = provider.detect(probe);
|
|
69
|
+
if (roots.length > 0) out.push({ provider, roots });
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The first role a provider gives this path. With `active` (the graph's record),
|
|
76
|
+
* only those providers are asked, each about the path relative to its package
|
|
77
|
+
* roots; without it, every registered provider is asked about the path as is.
|
|
78
|
+
*/
|
|
79
|
+
export function providerRole(
|
|
80
|
+
p: string,
|
|
81
|
+
active?: readonly ActiveProviderRecord[],
|
|
82
|
+
registry: readonly ConventionProvider[] = PROVIDERS,
|
|
83
|
+
): FileRole | undefined {
|
|
84
|
+
if (!active) {
|
|
85
|
+
for (const provider of registry) {
|
|
86
|
+
const role = provider.roleOf?.(p);
|
|
87
|
+
if (role) return role;
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
for (const rec of active) {
|
|
92
|
+
const provider = registry.find((x) => x.id === rec.id);
|
|
93
|
+
if (!provider?.roleOf) continue;
|
|
94
|
+
for (const root of rec.roots) {
|
|
95
|
+
if (root !== '' && !p.startsWith(`${root}/`)) continue;
|
|
96
|
+
const role = provider.roleOf(root === '' ? p : p.slice(root.length + 1));
|
|
97
|
+
if (role) return role;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
@@ -15,9 +15,13 @@
|
|
|
15
15
|
* rule cannot settle — a closure handler, a view name built at runtime — is
|
|
16
16
|
* counted in `stats`, never guessed.
|
|
17
17
|
*/
|
|
18
|
-
import
|
|
19
|
-
|
|
20
|
-
import type {
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
|
|
20
|
+
import type { SourceFile } from '../build.js';
|
|
21
|
+
import type { FileRole } from '../map_model.js';
|
|
22
|
+
import { psr4RulesFrom, resolvePsr4, type ResolutionConfig } from '../resolution_tiers.js';
|
|
23
|
+
import type { CodeEdge, CodeNode } from '../types.js';
|
|
24
|
+
import type { ConventionProvider, ProjectProbe } from './types.js';
|
|
21
25
|
|
|
22
26
|
export interface LaravelOverlayStats {
|
|
23
27
|
routes: number;
|
|
@@ -172,6 +176,8 @@ export function buildLaravelOverlay(
|
|
|
172
176
|
files: readonly SourceFile[],
|
|
173
177
|
baseNodes: readonly CodeNode[],
|
|
174
178
|
resolution: ResolutionConfig,
|
|
179
|
+
/** The package root with a trailing slash (`''` at the repository root). */
|
|
180
|
+
prefix = '',
|
|
175
181
|
): LaravelOverlay {
|
|
176
182
|
const stats: LaravelOverlayStats = {
|
|
177
183
|
routes: 0,
|
|
@@ -225,7 +231,8 @@ export function buildLaravelOverlay(
|
|
|
225
231
|
const viewFile = (name: string): string | null => {
|
|
226
232
|
if (name.includes('::')) return null;
|
|
227
233
|
const rel = name.replace(/\./g, '/');
|
|
228
|
-
|
|
234
|
+
const views = `${prefix}resources/views/`;
|
|
235
|
+
for (const c of [`${views}${rel}.blade.php`, `${views}${rel}.php`, `${views}${rel}/index.blade.php`]) {
|
|
229
236
|
if (fileIds.has(c)) return c;
|
|
230
237
|
}
|
|
231
238
|
return null;
|
|
@@ -234,7 +241,7 @@ export function buildLaravelOverlay(
|
|
|
234
241
|
|
|
235
242
|
// Routes.
|
|
236
243
|
let routeOrdinal = new Map<string, number>();
|
|
237
|
-
for (const f of php.filter((x) => x.path.startsWith(
|
|
244
|
+
for (const f of php.filter((x) => x.path.startsWith(`${prefix}routes/`))) {
|
|
238
245
|
const src = f.source;
|
|
239
246
|
const at = lineIndex(src);
|
|
240
247
|
const scope = phpScope(src);
|
|
@@ -433,12 +440,47 @@ export function buildLaravelOverlay(
|
|
|
433
440
|
const file = classFile(fq);
|
|
434
441
|
const target = file ? `${file}#${shortName(fq)}` : null;
|
|
435
442
|
if (!target || !nodeById.has(target)) continue;
|
|
436
|
-
emit(enclosing(d.file.path, at(m.index as number)), target, 'relates', '
|
|
443
|
+
emit(enclosing(d.file.path, at(m.index as number)), target, 'relates', 'orm-relation');
|
|
437
444
|
stats.relations += 1;
|
|
438
445
|
}
|
|
439
446
|
}
|
|
440
447
|
|
|
441
|
-
|
|
448
|
+
// Which models a method names. A controller imports its models once per
|
|
449
|
+
// file, so a route's tables are read per handler from these edges rather
|
|
450
|
+
// than from the file's `use` lines (which attribute every model of the file
|
|
451
|
+
// to every route the file serves).
|
|
452
|
+
const modelIds = new Set<string>();
|
|
453
|
+
for (const [id, d] of classDecl) {
|
|
454
|
+
const name = id.split('#')[1] as string;
|
|
455
|
+
if (modelNames.has(d.parent) && !MODEL_BASES.has(name) && nodeById.has(id)) modelIds.add(id);
|
|
456
|
+
}
|
|
457
|
+
if (modelIds.size > 0) {
|
|
458
|
+
for (const f of php) {
|
|
459
|
+
if (blade(f.path)) continue;
|
|
460
|
+
const scope = phpScope(f.source);
|
|
461
|
+
const lines = f.source.split('\n');
|
|
462
|
+
for (const n of symbolsByFile.get(f.path) ?? []) {
|
|
463
|
+
if (n.kind !== 'method') continue;
|
|
464
|
+
const [s0, , e0] = n.source_location as [number, number, number, number];
|
|
465
|
+
// Strings and comments name words, not classes: `'User not found'` is no reference.
|
|
466
|
+
const body = lines
|
|
467
|
+
.slice(s0 - 1, e0)
|
|
468
|
+
.join('\n')
|
|
469
|
+
.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|#[^\n[]*$/gm, ' ')
|
|
470
|
+
.replace(/'(?:[^'\\\n]|\\.)*'|"(?:[^"\\\n]|\\.)*"/g, "''");
|
|
471
|
+
const own = n.id.split('::')[0];
|
|
472
|
+
for (const token of new Set([...body.matchAll(/\b([A-Z]\w*)\b/g)].map((m) => m[1] as string))) {
|
|
473
|
+
const fq = qualify(token, scope);
|
|
474
|
+
const file = classFile(fq);
|
|
475
|
+
const target = file ? `${file}#${shortName(fq)}` : null;
|
|
476
|
+
if (!target || target === own || !modelIds.has(target)) continue;
|
|
477
|
+
emit(n.id, target, 'uses', scope.uses.has(token) ? 'import-specifier' : 'psr4');
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const routesFiles = new Set(php.filter((x) => x.path.startsWith(`${prefix}routes/`)).map((x) => x.path));
|
|
442
484
|
const dropEdge = (e: CodeEdge): boolean => {
|
|
443
485
|
if (e.relation !== 'calls' || e.resolved_via === 'same-file') return false;
|
|
444
486
|
const src = nodeById.get(e.source);
|
|
@@ -452,3 +494,67 @@ export function buildLaravelOverlay(
|
|
|
452
494
|
|
|
453
495
|
return { nodes, edges, dropEdge, stats };
|
|
454
496
|
}
|
|
497
|
+
|
|
498
|
+
/** A composer manifest that pulls in the framework or any of its components. */
|
|
499
|
+
function requiresLaravel(manifest: string): boolean {
|
|
500
|
+
try {
|
|
501
|
+
const j = JSON.parse(manifest) as { require?: Record<string, string>; 'require-dev'?: Record<string, string> };
|
|
502
|
+
const names = [...Object.keys(j.require ?? {}), ...Object.keys(j['require-dev'] ?? {})];
|
|
503
|
+
return names.some((n) => n === 'laravel/framework' || n.startsWith('illuminate/'));
|
|
504
|
+
} catch {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function detectLaravel(probe: ProjectProbe): string[] {
|
|
510
|
+
const roots = new Set<string>();
|
|
511
|
+
const dirOf = (p: string): string => {
|
|
512
|
+
const d = path.posix.dirname(p);
|
|
513
|
+
return d === '.' ? '' : d;
|
|
514
|
+
};
|
|
515
|
+
for (const [p, text] of probe.manifests) {
|
|
516
|
+
if (path.posix.basename(p) === 'composer.json' && requiresLaravel(text)) roots.add(dirOf(p));
|
|
517
|
+
}
|
|
518
|
+
for (const p of probe.markers) if (path.posix.basename(p) === 'artisan') roots.add(dirOf(p));
|
|
519
|
+
return [...roots].sort();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function laravelRole(p: string): FileRole | undefined {
|
|
523
|
+
if (path.posix.basename(p).endsWith('.blade.php') || p.startsWith('resources/views/') || p.includes('/resources/views/')) return 'view';
|
|
524
|
+
if (p.startsWith('routes/') || p === 'public/index.php' || p === 'artisan') return 'entry';
|
|
525
|
+
return undefined;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export const laravelProvider: ConventionProvider = {
|
|
529
|
+
id: 'laravel',
|
|
530
|
+
markers: ['artisan'],
|
|
531
|
+
detect: detectLaravel,
|
|
532
|
+
build: ({ files, baseNodes, resolution, roots, manifests }) => {
|
|
533
|
+
// One overlay per package root: a Laravel app under `backend/` keeps its
|
|
534
|
+
// routes, views and PSR-4 map there, not at the repository root.
|
|
535
|
+
const nodes = new Map<string, CodeNode>();
|
|
536
|
+
const edges = new Map<string, CodeEdge>();
|
|
537
|
+
const contests: Array<(e: CodeEdge) => boolean> = [];
|
|
538
|
+
for (const root of roots) {
|
|
539
|
+
const prefix = root === '' ? '' : `${root}/`;
|
|
540
|
+
let config = resolution;
|
|
541
|
+
if (root !== '') {
|
|
542
|
+
let composer: unknown = null;
|
|
543
|
+
try {
|
|
544
|
+
composer = JSON.parse(manifests.get(`${prefix}composer.json`) ?? 'null');
|
|
545
|
+
} catch {
|
|
546
|
+
composer = null;
|
|
547
|
+
}
|
|
548
|
+
const own = psr4RulesFrom(composer).map((r) => ({ ...r, dirs: r.dirs.map((d) => `${prefix}${d}`) }));
|
|
549
|
+
config = { ...resolution, psr4: [...own, ...resolution.psr4] };
|
|
550
|
+
}
|
|
551
|
+
const scoped = root === '' ? files : files.filter((f) => f.path.startsWith(prefix));
|
|
552
|
+
const o = buildLaravelOverlay(scoped, baseNodes, config, prefix);
|
|
553
|
+
for (const n of o.nodes) if (!nodes.has(n.id)) nodes.set(n.id, n);
|
|
554
|
+
for (const e of o.edges) edges.set(`${e.source}\0${e.relation}\0${e.target}`, e);
|
|
555
|
+
contests.push(o.dropEdge);
|
|
556
|
+
}
|
|
557
|
+
return { nodes: [...nodes.values()], edges: [...edges.values()], contest: (e) => contests.some((c) => c(e)) };
|
|
558
|
+
},
|
|
559
|
+
roleOf: laravelRole,
|
|
560
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NestJS: `@Controller('users')` + `@Get(':id')` → `GET /users/:id`
|
|
3
|
+
* --routes-to--> the decorated method; `constructor(private readonly s: UsersService)`
|
|
4
|
+
* in an `@Injectable()` / `@Controller()` class → class --injects--> UsersService
|
|
5
|
+
* when the file names that class. An object-form prefix (`@Controller({ path })`),
|
|
6
|
+
* an array of paths, and a custom provider token (`@Inject('X')`) are not
|
|
7
|
+
* followed; a global prefix set in `main.ts` is not applied.
|
|
8
|
+
*/
|
|
9
|
+
import { npmRoots } from './shared/detect.js';
|
|
10
|
+
import { annotationRoutes } from './shared/extractors.js';
|
|
11
|
+
import { constructorInjection } from './shared/js.js';
|
|
12
|
+
import { makeKit } from './shared/kit.js';
|
|
13
|
+
import type { ConventionProvider } from './types.js';
|
|
14
|
+
|
|
15
|
+
const EXTS = ['.ts', '.js'];
|
|
16
|
+
|
|
17
|
+
export const nestjsProvider: ConventionProvider = {
|
|
18
|
+
id: 'nestjs',
|
|
19
|
+
detect: (probe) => npmRoots(probe, ['@nestjs/core']),
|
|
20
|
+
build: (input) => {
|
|
21
|
+
const kit = makeKit(input);
|
|
22
|
+
annotationRoutes(kit, {
|
|
23
|
+
exts: EXTS,
|
|
24
|
+
route: /@(?<verb>Get|Post|Put|Patch|Delete|Options|Head|All)\(\s*(?:(['"`])(?<path>[^'"`]*)\2\s*)?\)/g,
|
|
25
|
+
verb: (m) => (m.groups?.['verb'] === 'All' ? 'ANY' : (m.groups?.['verb'] as string)),
|
|
26
|
+
prefix: /@Controller\(\s*(?:(['"`])(?<path>[^'"`]*)\1\s*)?\)/g,
|
|
27
|
+
});
|
|
28
|
+
constructorInjection(kit, {
|
|
29
|
+
exts: EXTS,
|
|
30
|
+
classDecorator: /@(?:Injectable|Controller|Resolver|WebSocketGateway)\(/,
|
|
31
|
+
});
|
|
32
|
+
return kit.result();
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js, both routers, under `app/` or `src/app/` and `pages/` or `src/pages/`
|
|
3
|
+
* of a package root:
|
|
4
|
+
*
|
|
5
|
+
* app/a/[id]/page.tsx PAGE /a/[id] --routes-to--> the page module
|
|
6
|
+
* app/api/x/route.ts GET /api/x · POST … --routes-to--> the exported verb function
|
|
7
|
+
* app/a/layout.tsx --includes--> every page below its directory
|
|
8
|
+
* pages/a.tsx PAGE /a --routes-to--> the page module
|
|
9
|
+
* pages/api/x.ts ROUTE /api/x --routes-to--> the API module
|
|
10
|
+
*
|
|
11
|
+
* Route ids match the default pass's, so a page drawn here replaces the generic
|
|
12
|
+
* one. Route groups and parallel slots drop out of the path; a private `_dir`
|
|
13
|
+
* is no route. A route module re-exporting a verb (`export { h as GET }`) is
|
|
14
|
+
* not followed, and the default pass still adds its generic `ROUTE` node for a
|
|
15
|
+
* `route.ts`.
|
|
16
|
+
*/
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
|
|
19
|
+
import { npmRoots } from './shared/detect.js';
|
|
20
|
+
import { cleanRoute, exportedVerbRoutes, underRoot } from './shared/js.js';
|
|
21
|
+
import { makeKit } from './shared/kit.js';
|
|
22
|
+
import type { ConventionProvider } from './types.js';
|
|
23
|
+
|
|
24
|
+
const PAGE_EXTS = new Set(['.tsx', '.jsx', '.ts', '.js', '.mdx']);
|
|
25
|
+
|
|
26
|
+
interface Classified {
|
|
27
|
+
kind: 'page' | 'route' | 'layout' | 'pages-page' | 'pages-api';
|
|
28
|
+
/** The directory the router starts at, repo-relative. */
|
|
29
|
+
base: string;
|
|
30
|
+
segments: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function classify(roots: readonly string[], p: string): Classified | null {
|
|
34
|
+
const at = underRoot(roots, p);
|
|
35
|
+
if (!at) return null;
|
|
36
|
+
const ext = path.posix.extname(p);
|
|
37
|
+
if (!PAGE_EXTS.has(ext)) return null;
|
|
38
|
+
const segs = at.rel.split('/');
|
|
39
|
+
const lead = segs[0] === 'src' ? 1 : 0;
|
|
40
|
+
const dir = segs[lead];
|
|
41
|
+
const prefix = [at.root, ...segs.slice(0, lead + 1)].filter(Boolean).join('/');
|
|
42
|
+
const stem = (segs[segs.length - 1] as string).slice(0, -ext.length);
|
|
43
|
+
const inner = segs.slice(lead + 1, -1);
|
|
44
|
+
if (dir === 'app') {
|
|
45
|
+
if (inner.some((s) => s.startsWith('_'))) return null;
|
|
46
|
+
if (stem === 'page') return { kind: 'page', base: prefix, segments: inner };
|
|
47
|
+
if (stem === 'route' && ext !== '.mdx') return { kind: 'route', base: prefix, segments: inner };
|
|
48
|
+
if (stem === 'layout') return { kind: 'layout', base: prefix, segments: inner };
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (dir === 'pages' && segs.length > lead + 1) {
|
|
52
|
+
if (stem.startsWith('_')) return null;
|
|
53
|
+
const rest = [...inner, stem];
|
|
54
|
+
return { kind: rest[0] === 'api' ? 'pages-api' : 'pages-page', base: prefix, segments: rest };
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const nextjsProvider: ConventionProvider = {
|
|
60
|
+
id: 'nextjs',
|
|
61
|
+
detect: (probe) => npmRoots(probe, ['next']),
|
|
62
|
+
build: (input) => {
|
|
63
|
+
const kit = makeKit(input);
|
|
64
|
+
const pages: string[] = [];
|
|
65
|
+
const layouts: string[] = [];
|
|
66
|
+
for (const p of kit.paths) {
|
|
67
|
+
const c = classify(kit.roots, p);
|
|
68
|
+
if (!c) continue;
|
|
69
|
+
const route = cleanRoute(c.segments);
|
|
70
|
+
if (c.kind === 'layout') {
|
|
71
|
+
layouts.push(p);
|
|
72
|
+
} else if (c.kind === 'route') {
|
|
73
|
+
if (kit.fileIds.has(p)) exportedVerbRoutes(kit, p, route);
|
|
74
|
+
} else {
|
|
75
|
+
kit.fileNode(p);
|
|
76
|
+
const id = kit.route(p, c.kind === 'pages-api' ? 'ROUTE' : 'PAGE', route, 1);
|
|
77
|
+
kit.emit(id, p, 'routes-to', 'route-table');
|
|
78
|
+
if (c.kind === 'page') pages.push(p);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const layout of layouts) {
|
|
82
|
+
const dir = `${path.posix.dirname(layout)}/`;
|
|
83
|
+
for (const page of pages) {
|
|
84
|
+
if (!page.startsWith(dir)) continue;
|
|
85
|
+
kit.fileNode(layout);
|
|
86
|
+
kit.emit(layout, page, 'includes', 'directive-scan');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return kit.result();
|
|
90
|
+
},
|
|
91
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nuxt file routing under a package root (or its `app/` directory):
|
|
3
|
+
*
|
|
4
|
+
* pages/a/[id].vue PAGE /a/[id] --routes-to--> the page component
|
|
5
|
+
* server/api/users.get.ts GET /api/users --routes-to--> the handler module
|
|
6
|
+
* server/routes/feed.ts ROUTE /feed --routes-to--> the handler module
|
|
7
|
+
*
|
|
8
|
+
* Unlike the default pass, no `index` page is required. Route ids match the
|
|
9
|
+
* default's, so a page drawn here replaces the generic one. Route middleware,
|
|
10
|
+
* `definePageMeta` path overrides and catch-all rewrites are not followed.
|
|
11
|
+
*/
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { npmRoots } from './shared/detect.js';
|
|
15
|
+
import { cleanRoute, HTTP_VERBS, underRoot } from './shared/js.js';
|
|
16
|
+
import { makeKit } from './shared/kit.js';
|
|
17
|
+
import type { ConventionProvider } from './types.js';
|
|
18
|
+
|
|
19
|
+
const SERVER_EXTS = new Set(['.ts', '.js', '.mjs']);
|
|
20
|
+
|
|
21
|
+
function classify(roots: readonly string[], p: string): { verb: string; route: string } | null {
|
|
22
|
+
const at = underRoot(roots, p);
|
|
23
|
+
if (!at) return null;
|
|
24
|
+
const segs = at.rel.split('/');
|
|
25
|
+
const lead = segs[0] === 'app' && segs[1] === 'pages' ? 1 : 0;
|
|
26
|
+
const ext = path.posix.extname(p);
|
|
27
|
+
const stem = (segs[segs.length - 1] as string).slice(0, -ext.length);
|
|
28
|
+
if (segs[lead] === 'pages' && segs.length > lead + 1 && ext === '.vue') {
|
|
29
|
+
return { verb: 'PAGE', route: cleanRoute([...segs.slice(lead + 1, -1), stem]) };
|
|
30
|
+
}
|
|
31
|
+
if (segs[0] === 'server' && (segs[1] === 'api' || segs[1] === 'routes') && segs.length > 2 && SERVER_EXTS.has(ext)) {
|
|
32
|
+
const dot = /^(?<name>.+)\.(?<verb>get|post|put|patch|delete|head|options)$/.exec(stem);
|
|
33
|
+
const verb = dot ? (dot.groups?.['verb'] as string).toUpperCase() : 'ROUTE';
|
|
34
|
+
const name = dot ? (dot.groups?.['name'] as string) : stem;
|
|
35
|
+
if (!(verb === 'ROUTE' || (HTTP_VERBS as readonly string[]).includes(verb))) return null;
|
|
36
|
+
const prefix = segs[1] === 'api' ? ['api'] : [];
|
|
37
|
+
return { verb, route: cleanRoute([...prefix, ...segs.slice(2, -1), name]) };
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const nuxtProvider: ConventionProvider = {
|
|
43
|
+
id: 'nuxt',
|
|
44
|
+
detect: (probe) => npmRoots(probe, ['nuxt']),
|
|
45
|
+
build: (input) => {
|
|
46
|
+
const kit = makeKit(input);
|
|
47
|
+
for (const p of kit.paths) {
|
|
48
|
+
const r = classify(kit.roots, p);
|
|
49
|
+
if (!r) continue;
|
|
50
|
+
kit.fileNode(p);
|
|
51
|
+
kit.emit(kit.route(p, r.verb, r.route, 1), p, 'routes-to', 'route-table');
|
|
52
|
+
}
|
|
53
|
+
return kit.result();
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Router: `<Route path="/x" element={<X />} />` (or `component={X}`) and
|
|
3
|
+
* route objects `{ path: '/x', element: <X />, Component: X, lazy: () => import('./x') }`
|
|
4
|
+
* → a PAGE route --routes-to--> the component the file names or the module it
|
|
5
|
+
* imports lazily. A `<Route>` without `path` (an index or layout route) and an
|
|
6
|
+
* object without a component key are no routes. Nested `children` paths are
|
|
7
|
+
* not joined to their parent's.
|
|
8
|
+
*/
|
|
9
|
+
import { npmRoots } from './shared/detect.js';
|
|
10
|
+
import { joinRoute } from './shared/extractors.js';
|
|
11
|
+
import { componentTarget, routeObjects } from './shared/js.js';
|
|
12
|
+
import { type Kit, lineIndex, makeKit } from './shared/kit.js';
|
|
13
|
+
import type { ConventionProvider } from './types.js';
|
|
14
|
+
|
|
15
|
+
const EXTS = ['.tsx', '.jsx', '.ts', '.js'];
|
|
16
|
+
|
|
17
|
+
/** The attribute text of a JSX tag opened at `from`, up to its `>` outside braces. */
|
|
18
|
+
function attributes(src: string, from: number): string {
|
|
19
|
+
let depth = 0;
|
|
20
|
+
for (let i = from; i < src.length && i < from + 2000; i++) {
|
|
21
|
+
const c = src[i];
|
|
22
|
+
if (c === '{') depth += 1;
|
|
23
|
+
else if (c === '}') depth -= 1;
|
|
24
|
+
else if (c === '>' && depth === 0) return src.slice(from, i);
|
|
25
|
+
}
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function jsxRoutes(kit: Kit): number {
|
|
30
|
+
let count = 0;
|
|
31
|
+
for (const f of kit.filesWithExt('.tsx', '.jsx', '.js')) {
|
|
32
|
+
const at = lineIndex(f.source);
|
|
33
|
+
for (const m of f.source.matchAll(/<Route\b/g)) {
|
|
34
|
+
const attrs = attributes(f.source, (m.index as number) + m[0].length);
|
|
35
|
+
const p = /\bpath\s*=\s*\{?\s*(['"`])([^'"`$]*)\1/.exec(attrs);
|
|
36
|
+
if (!p) continue;
|
|
37
|
+
const id = kit.route(f.path, 'PAGE', joinRoute(p[2]), at(m.index as number));
|
|
38
|
+
const el = /\belement\s*=\s*\{\s*(<\s*[A-Z][\w$]*)/.exec(attrs);
|
|
39
|
+
const comp = /\b(?:component|Component)\s*=\s*\{\s*([A-Za-z_$][\w$]*)\s*\}/.exec(attrs);
|
|
40
|
+
const value = el?.[1] ?? comp?.[1];
|
|
41
|
+
const target = value ? componentTarget(kit, f.path, value) : null;
|
|
42
|
+
if (target) kit.emit(id, target, 'routes-to', 'route-table');
|
|
43
|
+
count += 1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return count;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const reactRouterProvider: ConventionProvider = {
|
|
50
|
+
id: 'react-router',
|
|
51
|
+
detect: (probe) => npmRoots(probe, ['react-router', 'react-router-dom']),
|
|
52
|
+
build: (input) => {
|
|
53
|
+
const kit = makeKit(input);
|
|
54
|
+
jsxRoutes(kit);
|
|
55
|
+
routeObjects(kit, { exts: EXTS, componentKeys: ['element', 'Component', 'lazy'] });
|
|
56
|
+
return kit.result();
|
|
57
|
+
},
|
|
58
|
+
};
|