@event4u/agent-config 17.2.1 → 17.4.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 +167 -193
- package/CREDITS.md +2 -2
- package/README.md +1 -1
- package/dist/agent-src/skills/code-intelligence/SKILL.md +16 -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/registry.js +2 -2
- package/dist/cli-delegate/{chunk-KR5ZIFPO.js → chunk-5AYJGUA3.js} +1 -3
- package/dist/cli-delegate/chunk-DIFZBP4G.js +1336 -0
- package/dist/cli-delegate/chunk-E7ACTJTJ.js +6792 -0
- package/dist/cli-delegate/{chunk-VYZGJLCA.js → chunk-RKNT4LIK.js} +22 -1286
- package/dist/cli-delegate/{chunk-BZJ4Y3CI.js → chunk-WY5ZZPGL.js} +2 -2
- package/dist/cli-delegate/{chunk-CMZEKSNN.js → chunk-X6LZ7W5A.js} +73 -10
- package/dist/cli-delegate/cmd_conformance.js +190 -192
- package/dist/cli-delegate/cmd_doctor.js +5 -5
- package/dist/cli-delegate/cmd_doctor_neighbours.js +3 -3
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +2 -2
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.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_update.js +24 -13
- package/dist/cli-delegate/cmd_upgrade.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/cli-delegate/dist-25AZAHOP.js +3240 -0
- package/dist/cli-delegate/dist-P7MFPMF3.js +411 -0
- package/dist/cli-delegate/dist-XAS5ZLQB.js +6 -0
- package/dist/cli-delegate/{upgrade_unattended-VVL6JLSX.js → upgrade_unattended-QCWOKTUN.js} +178 -163
- 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 +443 -0
- package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -0
- package/dist/hooks/dispatch.js +1001 -938
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/hooks/egress_engine.chunk.cjs +57 -7
- package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
- package/dist/hooks/update_notice.chunk.cjs +914 -0
- package/dist/hooks/update_notice.chunk.cjs.sha256 +1 -0
- 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 +71202 -63282
- package/dist/scripts/_lib/egress_rules.js +8 -2
- package/dist/scripts/condense.js +18 -15
- 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 +6 -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/evaluator-budgets.json +9 -8
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/gitignore-block.txt +6 -5
- package/src/config/hook-bundle-budget.json +46 -6
- package/src/config/pack-size-budget.json +12 -1
- package/src/config/packed-binary-manifest.json +18 -0
- package/src/config/sigstore/npm_keys.json +50 -0
- package/src/config/sigstore/trust-material.json +9 -0
- package/src/config/sigstore/trusted_root.json +126 -0
- package/src/config/skill-body-budget.json +7 -1
- package/src/scripts/_cli/cmd_settings_set.ts +3 -2
- package/src/scripts/_cli/cmd_update.ts +48 -20
- package/src/scripts/_cli/cmd_upgrade.ts +7 -1
- package/src/scripts/_cli/upgrade_unattended.ts +90 -76
- package/src/scripts/_dispatch.bash +4 -3
- package/src/scripts/_lib/confusables.ts +0 -19
- package/src/scripts/_lib/egress_normalize.ts +53 -23
- package/src/scripts/_lib/egress_rules.ts +8 -2
- package/src/scripts/_lib/egress_text.ts +5 -3
- package/src/scripts/_lib/egress_token.ts +7 -5
- package/src/scripts/_lib/gate_collector_experimental.ts +633 -0
- package/src/scripts/_lib/git_common_dir.ts +10 -0
- package/src/scripts/_lib/graph_feeder_record.ts +3 -2
- package/src/scripts/_lib/install_reach_checks.ts +32 -2
- package/src/scripts/_lib/lineage_backfill.ts +136 -0
- package/src/scripts/_lib/lineage_ledger.ts +211 -0
- package/src/scripts/_lib/lineage_map.ts +55 -0
- package/src/scripts/_lib/lineage_query.ts +140 -0
- package/src/scripts/_lib/lineage_seed.ts +116 -0
- package/src/scripts/_lib/lineage_watch.ts +249 -0
- package/src/scripts/_lib/link_crypto.ts +8 -4
- package/src/scripts/_lib/listing_manifests.ts +85 -0
- package/src/scripts/_lib/neighbour_census.ts +1 -0
- package/src/scripts/_lib/production_reach.ts +3 -3
- package/src/scripts/_lib/project_registry.ts +227 -0
- package/src/scripts/_lib/provenance_verify.ts +14 -242
- package/src/scripts/_lib/release_settings_surface.ts +2 -6
- package/src/scripts/_lib/sigstore_provenance.ts +177 -0
- package/src/scripts/_lib/source_id.ts +48 -0
- package/src/scripts/_lib/source_shape.ts +4 -0
- package/src/scripts/_lib/unattended_gates.ts +6 -1
- package/src/scripts/adoption_status.ts +231 -27
- package/src/scripts/advisories.ts +26 -10
- package/src/scripts/ai_council/council_egress.ts +5 -6
- package/src/scripts/bench_graph_feeder_latency.ts +10 -3
- package/src/scripts/build_mcp_registry_manifest.ts +23 -12
- package/src/scripts/check_dependency_floors.ts +14 -0
- package/src/scripts/check_egress_mutations.ts +1 -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 +145 -45
- package/src/scripts/code_graph/cli.ts +382 -35
- package/src/scripts/code_graph/conventions_default.ts +247 -0
- package/src/scripts/code_graph/detect.ts +5 -5
- package/src/scripts/code_graph/estate_cli.ts +135 -0
- package/src/scripts/code_graph/estate_index.ts +394 -0
- package/src/scripts/code_graph/estate_manifests.ts +223 -0
- package/src/scripts/code_graph/estate_map.ts +141 -0
- package/src/scripts/code_graph/extract.ts +8 -2
- package/src/scripts/code_graph/extract_more.ts +241 -2
- package/src/scripts/code_graph/graph_budget.ts +158 -0
- package/src/scripts/code_graph/graph_home.ts +80 -0
- package/src/scripts/code_graph/lineage_cli.ts +315 -0
- package/src/scripts/code_graph/loader.ts +10 -2
- package/src/scripts/code_graph/map.ts +195 -53
- package/src/scripts/code_graph/map_model.ts +113 -20
- package/src/scripts/code_graph/map_verify.ts +80 -5
- package/src/scripts/code_graph/map_viewer/viewer.css +18 -0
- package/src/scripts/code_graph/map_viewer/viewer.js +146 -15
- package/src/scripts/code_graph/passes.ts +173 -0
- package/src/scripts/code_graph/project_ref.ts +25 -0
- package/src/scripts/code_graph/projects_cli.ts +254 -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 +299 -0
- package/src/scripts/code_graph/providers/django.ts +206 -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 +103 -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/rails.ts +204 -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 +175 -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/query.ts +9 -9
- package/src/scripts/code_graph/refresh_cli.ts +81 -0
- package/src/scripts/code_graph/refresh_lock.ts +197 -0
- package/src/scripts/code_graph/repo_identity.ts +169 -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 +31 -28
- package/src/scripts/code_graph/types.ts +79 -41
- package/src/scripts/code_graph/validate.ts +6 -8
- package/src/scripts/condense.ts +15 -15
- package/src/scripts/generate_settings_reference.ts +2 -6
- package/src/scripts/hook_chunks.mjs +11 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +43 -23
- package/src/scripts/hooks/code_graph_refresh_hook.ts +163 -0
- package/src/scripts/hooks/dispatch_hook.ts +6 -7
- package/src/scripts/hooks/host_semantics.ts +72 -0
- package/src/scripts/hooks/lazy_concern.ts +8 -0
- package/src/scripts/hooks/user_notice.ts +105 -0
- package/src/scripts/install-hooks.sh +54 -33
- package/src/scripts/install.ts +2 -6
- package/src/scripts/lint_harvest_provenance.ts +137 -11
- 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 +70 -15
- package/src/scripts/mcp_server/graph_tools.ts +165 -41
- package/src/scripts/network_sources.ts +24 -6
- 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/scripts/schemas/provenance-mechanism.schema.json +46 -0
- package/src/scripts/update_notice_hook.ts +75 -7
- 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 +8 -6
- package/src/vendor/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-ruby.wasm +0 -0
- package/dist/cli-delegate/chunk-XIAPY44N.js +0 -4587
- package/dist/ui/assets/index-w70Ado09.js +0 -23
- /package/dist/cli-delegate/{chunk-FKYZOWVD.js → chunk-QBAFP67O.js} +0 -0
- /package/dist/cli-delegate/{chunk-BMGUGSY6.js → chunk-TQG6UMPC.js} +0 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ruby on Rails: the links a Rails application makes by convention.
|
|
3
|
+
*
|
|
4
|
+
* config/routes.rb get '/x', to: 'users#index' · post 'x' => 'users#create'
|
|
5
|
+
* · root 'pages#home' · resources :users, only:/except:
|
|
6
|
+
* · namespace :admin do … end route --routes-to--> the action
|
|
7
|
+
* app/models class User < ApplicationRecord class --persists--> table:users
|
|
8
|
+
* self.table_name = "t" wins; a subclass of a model shares its table (STI)
|
|
9
|
+
* app/models has_many / has_one / belongs_to (class_name: wins) class --relates--> model
|
|
10
|
+
* db/migrate create_table :t file --migrates--> table:t
|
|
11
|
+
* a controller action app/views/<controller>/<action>.* action --renders--> the view
|
|
12
|
+
*
|
|
13
|
+
* `resources` expands into the seven REST actions the controller actually
|
|
14
|
+
* defines, within `only:` / `except:`; an explicit route keeps its node when
|
|
15
|
+
* the action is missing. The table name is the class in snake_case, pluralised
|
|
16
|
+
* by the common English rules plus Rails' irregulars; an `inflections` file is
|
|
17
|
+
* not read. Not followed: `resource` (singular), nested `resources` blocks,
|
|
18
|
+
* `scope` / `constraints`, `concerns`, an explicit `render`, and a model
|
|
19
|
+
* outside `app/models`.
|
|
20
|
+
*/
|
|
21
|
+
import * as path from 'node:path';
|
|
22
|
+
|
|
23
|
+
import type { CodeNode } from '../types.js';
|
|
24
|
+
import { gemRoots } from './shared/detect.js';
|
|
25
|
+
import { joinRoute } from './shared/extractors.js';
|
|
26
|
+
import { type Kit, lineIndex, makeKit } from './shared/kit.js';
|
|
27
|
+
import type { ConventionProvider } from './types.js';
|
|
28
|
+
|
|
29
|
+
const REST: ReadonlyArray<[action: string, verb: string, suffix: string]> = [
|
|
30
|
+
['index', 'GET', ''],
|
|
31
|
+
['new', 'GET', 'new'],
|
|
32
|
+
['create', 'POST', ''],
|
|
33
|
+
['show', 'GET', ':id'],
|
|
34
|
+
['edit', 'GET', ':id/edit'],
|
|
35
|
+
['update', 'PATCH', ':id'],
|
|
36
|
+
['destroy', 'DELETE', ':id'],
|
|
37
|
+
];
|
|
38
|
+
const IRREGULAR: Readonly<Record<string, string>> = { person: 'people', man: 'men', woman: 'women', child: 'children', mouse: 'mice' };
|
|
39
|
+
const VERB_ROUTE = /^\s*(get|post|put|patch|delete)\s+['"]([^'"]*)['"]\s*(?:,\s*to:\s*|=>\s*)['"](\w[\w/]*)#(\w+)['"]/;
|
|
40
|
+
const ROOT_ROUTE = /^\s*root\s+(?:to:\s*)?['"](\w[\w/]*)#(\w+)['"]/;
|
|
41
|
+
const RESOURCES = /^\s*resources\s+:(\w+)(.*)$/;
|
|
42
|
+
const NAMESPACE = /^\s*namespace\s+:(\w+)\s+do\b/;
|
|
43
|
+
const OPENS_BLOCK = /\bdo(\s*\|[^|]*\|)?\s*$/;
|
|
44
|
+
|
|
45
|
+
function snake(name: string): string {
|
|
46
|
+
return name.replace(/([a-z\d])([A-Z])/g, '$1_$2').replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2').toLowerCase();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function pluralize(word: string): string {
|
|
50
|
+
const head = word.slice(0, word.lastIndexOf('_') + 1);
|
|
51
|
+
const last = word.slice(head.length);
|
|
52
|
+
if (IRREGULAR[last]) return head + IRREGULAR[last];
|
|
53
|
+
if (/[^aeiou]y$/.test(last)) return head + last.slice(0, -1) + 'ies';
|
|
54
|
+
if (/(s|x|z|ch|sh)$/.test(last)) return head + last + 'es';
|
|
55
|
+
return head + last + 's';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function singularize(word: string): string {
|
|
59
|
+
for (const [one, many] of Object.entries(IRREGULAR)) if (word === many) return one;
|
|
60
|
+
if (/ies$/.test(word)) return word.slice(0, -3) + 'y';
|
|
61
|
+
if (/(s|x|z|ch|sh)es$/.test(word)) return word.slice(0, -2);
|
|
62
|
+
return word.endsWith('s') ? word.slice(0, -1) : word;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function camel(snakeName: string): string {
|
|
66
|
+
return snakeName.replace(/(^|_)([a-z\d])/g, (_m, _s, c: string) => c.toUpperCase());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The action method of the controller `ctrl` (`admin/reports`) under `root`, if the controller defines it. */
|
|
70
|
+
function action(kit: Kit, root: string, ctrl: string, name: string): CodeNode | null {
|
|
71
|
+
const file = path.posix.join(root, 'app/controllers', `${ctrl}_controller.rb`);
|
|
72
|
+
return kit.nodesInFile(file).find((n) => n.kind === 'method' && n.label === name) ?? null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function routes(kit: Kit, root: string): void {
|
|
76
|
+
const file = path.posix.join(root, 'config/routes.rb');
|
|
77
|
+
const src = kit.files.find((f) => f.path === file)?.source;
|
|
78
|
+
if (src === undefined) return;
|
|
79
|
+
// One frame per open `do` block: a namespace adds a path and a controller
|
|
80
|
+
// directory; any other block only keeps `end` balanced.
|
|
81
|
+
const frames: Array<string | null> = [];
|
|
82
|
+
const ns = (): string[] => frames.filter((f): f is string => f !== null);
|
|
83
|
+
const declare = (verb: string, routePath: string, ctrl: string, act: string, line: number, keepWithoutAction: boolean): void => {
|
|
84
|
+
const target = action(kit, root, [...ns(), ctrl].join('/'), act);
|
|
85
|
+
if (!target && !keepWithoutAction) return;
|
|
86
|
+
const id = kit.route(file, verb, joinRoute(...ns(), routePath), line);
|
|
87
|
+
if (target) kit.emit(id, target.id, 'routes-to', 'route-table');
|
|
88
|
+
};
|
|
89
|
+
src.split('\n').forEach((raw, i) => {
|
|
90
|
+
const line = i + 1;
|
|
91
|
+
// Whole-line comments only: `#` is also the controller#action separator.
|
|
92
|
+
if (/^\s*#/.test(raw)) return;
|
|
93
|
+
const text = raw;
|
|
94
|
+
if (/^\s*end\b/.test(text)) {
|
|
95
|
+
frames.pop();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
let m = NAMESPACE.exec(text);
|
|
99
|
+
if (m) {
|
|
100
|
+
frames.push(m[1] as string);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if ((m = ROOT_ROUTE.exec(text))) declare('GET', '/', m[1] as string, m[2] as string, line, true);
|
|
104
|
+
else if ((m = VERB_ROUTE.exec(text))) declare((m[1] as string).toUpperCase(), m[2] as string, m[3] as string, m[4] as string, line, true);
|
|
105
|
+
else if ((m = RESOURCES.exec(text))) {
|
|
106
|
+
const name = m[1] as string;
|
|
107
|
+
const opts = m[2] as string;
|
|
108
|
+
const list = (key: string): string[] | null => {
|
|
109
|
+
const hit = new RegExp(`\\b${key}:\\s*\\[([^\\]]*)\\]`).exec(opts) ?? new RegExp(`\\b${key}:\\s*:(\\w+)`).exec(opts);
|
|
110
|
+
return hit ? [...(hit[1] as string).matchAll(/:?(\w+)/g)].map((x) => x[1] as string) : null;
|
|
111
|
+
};
|
|
112
|
+
const only = list('only');
|
|
113
|
+
const except = list('except') ?? [];
|
|
114
|
+
for (const [act, verb, suffix] of REST) {
|
|
115
|
+
if ((only && !only.includes(act)) || except.includes(act)) continue;
|
|
116
|
+
declare(verb, joinRoute(name, suffix), name, act, line, false);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (OPENS_BLOCK.test(text)) frames.push(null);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function models(kit: Kit, root: string): void {
|
|
124
|
+
const dir = path.posix.join(root, 'app/models') + '/';
|
|
125
|
+
const files = kit.filesWithExt('.rb').filter((f) => f.path.startsWith(dir));
|
|
126
|
+
const classes = files.flatMap((f) =>
|
|
127
|
+
kit.nodesInFile(f.path).filter((n): n is CodeNode => n.kind === 'class').map((node) => {
|
|
128
|
+
const head = f.source.split('\n')[(node.source_location[0] as number) - 1] ?? '';
|
|
129
|
+
const parent = /<\s*([\w:]+)/.exec(head)?.[1]?.split('::').pop() ?? '';
|
|
130
|
+
const span = f.source.split('\n').slice((node.source_location[0] as number) - 1, node.source_location[2] as number).join('\n');
|
|
131
|
+
return { node, file: f, parent, span };
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
const byName = new Map(classes.map((c) => [c.node.label, c] as const));
|
|
135
|
+
const tableOf = new Map<string, string | null>();
|
|
136
|
+
const resolveTable = (name: string, seen: Set<string> = new Set()): string | null => {
|
|
137
|
+
if (tableOf.has(name)) return tableOf.get(name) as string | null;
|
|
138
|
+
const c = byName.get(name);
|
|
139
|
+
if (!c || seen.has(name)) return null;
|
|
140
|
+
seen.add(name);
|
|
141
|
+
const explicit = /self\.table_name\s*=\s*['"]([^'"]+)['"]/.exec(c.span)?.[1];
|
|
142
|
+
const abstract = /self\.abstract_class\s*=\s*true/.test(c.span);
|
|
143
|
+
let table: string | null = null;
|
|
144
|
+
if (explicit) table = explicit;
|
|
145
|
+
else if (abstract) table = null;
|
|
146
|
+
else if (c.parent === 'ApplicationRecord' || c.parent === 'Base') table = pluralize(snake(name));
|
|
147
|
+
else if (byName.has(c.parent)) table = resolveTable(c.parent, seen);
|
|
148
|
+
tableOf.set(name, table);
|
|
149
|
+
return table;
|
|
150
|
+
};
|
|
151
|
+
for (const c of classes) {
|
|
152
|
+
const table = resolveTable(c.node.label);
|
|
153
|
+
if (table) kit.emit(c.node.id, kit.table(table, c.file.path, c.node.source_location[0] as number), 'persists', 'schema-call');
|
|
154
|
+
for (const m of c.span.matchAll(/^\s*(has_many|has_one|belongs_to|has_and_belongs_to_many)\s+:(\w+)([^\n]*)/gm)) {
|
|
155
|
+
const named = /class_name:\s*['"]([\w:]+)['"]/.exec(m[3] as string)?.[1]?.split('::').pop();
|
|
156
|
+
const assoc = m[2] as string;
|
|
157
|
+
const target = byName.get(named ?? camel(m[1] === 'has_many' || m[1] === 'has_and_belongs_to_many' ? singularize(assoc) : assoc));
|
|
158
|
+
if (target && target.node.id !== c.node.id) kit.emit(c.node.id, target.node.id, 'relates', 'orm-relation');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function migrations(kit: Kit, root: string): void {
|
|
164
|
+
const dir = path.posix.join(root, 'db/migrate') + '/';
|
|
165
|
+
for (const f of kit.filesWithExt('.rb').filter((x) => x.path.startsWith(dir))) {
|
|
166
|
+
const at = lineIndex(f.source);
|
|
167
|
+
for (const m of f.source.matchAll(/\bcreate_table\s+(?::(\w+)|['"]([^'"]+)['"])/g)) {
|
|
168
|
+
const table = (m[1] ?? m[2]) as string;
|
|
169
|
+
kit.emit(f.path, kit.table(table, f.path, at(m.index as number)), 'migrates', 'schema-call');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function views(kit: Kit, root: string): void {
|
|
175
|
+
const ctrlDir = path.posix.join(root, 'app/controllers') + '/';
|
|
176
|
+
const viewDir = path.posix.join(root, 'app/views');
|
|
177
|
+
for (const f of kit.filesWithExt('.rb').filter((x) => x.path.startsWith(ctrlDir) && x.path.endsWith('_controller.rb'))) {
|
|
178
|
+
const ctrl = f.path.slice(ctrlDir.length, -'_controller.rb'.length);
|
|
179
|
+
for (const n of kit.nodesInFile(f.path)) {
|
|
180
|
+
if (n.kind !== 'method') continue;
|
|
181
|
+
const prefix = `${viewDir}/${ctrl}/${n.label}.`;
|
|
182
|
+
const hits = kit.paths.filter((p) => p.startsWith(prefix) && !p.slice(prefix.length).includes('/'));
|
|
183
|
+
if (hits.length !== 1) continue;
|
|
184
|
+
const target = hits[0] as string;
|
|
185
|
+
kit.fileNode(target);
|
|
186
|
+
kit.emit(n.id, target, 'renders', 'directive-scan');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export const railsProvider: ConventionProvider = {
|
|
192
|
+
id: 'rails',
|
|
193
|
+
detect: (probe) => gemRoots(probe, 'rails'),
|
|
194
|
+
build: (input) => {
|
|
195
|
+
const kit = makeKit(input);
|
|
196
|
+
for (const root of kit.roots) {
|
|
197
|
+
routes(kit, root);
|
|
198
|
+
models(kit, root);
|
|
199
|
+
migrations(kit, root);
|
|
200
|
+
views(kit, root);
|
|
201
|
+
}
|
|
202
|
+
return kit.result();
|
|
203
|
+
},
|
|
204
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection by manifest, per package root. Every helper returns the sorted
|
|
3
|
+
* roots (`''` for the repository root) whose manifest declares one of the
|
|
4
|
+
* named packages — never a guess from file suffixes.
|
|
5
|
+
*/
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
|
|
8
|
+
import type { ProjectProbe } from '../types.js';
|
|
9
|
+
|
|
10
|
+
function rootOf(p: string): string {
|
|
11
|
+
const d = path.posix.dirname(p);
|
|
12
|
+
return d === '.' ? '' : d;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function rootsWhere(probe: ProjectProbe, basename: RegExp, declares: (text: string) => boolean): string[] {
|
|
16
|
+
const roots = new Set<string>();
|
|
17
|
+
for (const [p, text] of probe.manifests) if (basename.test(path.posix.basename(p)) && declares(text)) roots.add(rootOf(p));
|
|
18
|
+
return [...roots].sort();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function jsonDeps(text: string, keys: readonly string[]): string[] {
|
|
22
|
+
try {
|
|
23
|
+
const j = JSON.parse(text) as Record<string, Record<string, string> | undefined>;
|
|
24
|
+
return keys.flatMap((k) => Object.keys(j[k] ?? {}));
|
|
25
|
+
} catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const matches = (names: readonly (string | RegExp)[], dep: string): boolean =>
|
|
31
|
+
names.some((n) => (typeof n === 'string' ? n === dep : n.test(dep)));
|
|
32
|
+
|
|
33
|
+
/** `package.json` dependencies, devDependencies or peerDependencies. */
|
|
34
|
+
export function npmRoots(probe: ProjectProbe, names: readonly (string | RegExp)[]): string[] {
|
|
35
|
+
return rootsWhere(probe, /^package\.json$/, (t) =>
|
|
36
|
+
jsonDeps(t, ['dependencies', 'devDependencies', 'peerDependencies']).some((d) => matches(names, d)),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `composer.json` require or require-dev. */
|
|
41
|
+
export function composerRoots(probe: ProjectProbe, names: readonly (string | RegExp)[]): string[] {
|
|
42
|
+
return rootsWhere(probe, /^composer\.json$/, (t) => jsonDeps(t, ['require', 'require-dev']).some((d) => matches(names, d)));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** `pyproject.toml` or a `requirements*.txt` naming the distribution. */
|
|
46
|
+
export function pythonRoots(probe: ProjectProbe, names: readonly string[]): string[] {
|
|
47
|
+
const re = new RegExp(`(^|["'\\s\\[,])(${names.map((n) => n.replace(/[-_.]/g, '[-_.]')).join('|')})(?=[\\s"'<>=~!;\\[,]|$)`, 'im');
|
|
48
|
+
return rootsWhere(probe, /^(pyproject\.toml|requirements[\w.-]*\.txt)$/, (t) => re.test(t));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A Maven `pom.xml` or Gradle build file naming the artifact. */
|
|
52
|
+
export function jvmRoots(probe: ProjectProbe, artifact: RegExp): string[] {
|
|
53
|
+
return rootsWhere(probe, /^(pom\.xml|build\.gradle(\.kts)?)$/, (t) => artifact.test(t));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A `go.mod` requiring the module. */
|
|
57
|
+
export function goRoots(probe: ProjectProbe, modules: readonly string[]): string[] {
|
|
58
|
+
return rootsWhere(probe, /^go\.mod$/, (t) => modules.some((m) => t.includes(m)));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A `*.csproj` referencing the SDK or package. */
|
|
62
|
+
export function dotnetRoots(probe: ProjectProbe, marker: RegExp): string[] {
|
|
63
|
+
return rootsWhere(probe, /\.csproj$/, (t) => marker.test(t));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A `Gemfile` declaring the gem. */
|
|
67
|
+
export function gemRoots(probe: ProjectProbe, gem: string): string[] {
|
|
68
|
+
return rootsWhere(probe, /^Gemfile$/, (t) => new RegExp(`^\\s*gem\\s+['"]${gem}['"]`, 'm').test(t));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Marker files present in the probe, by basename, as roots. */
|
|
72
|
+
export function markerRoots(probe: ProjectProbe, basename: string): string[] {
|
|
73
|
+
return [...new Set([...probe.markers].filter((p) => path.posix.basename(p) === basename).map(rootOf))].sort();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function union(...lists: string[][]): string[] {
|
|
77
|
+
return [...new Set(lists.flat())].sort();
|
|
78
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The five shared mechanisms most providers are a parameter table for
|
|
3
|
+
* (road-to-graph-providers-for-every-stack 4.1):
|
|
4
|
+
*
|
|
5
|
+
* annotationRoutes @Get('/x') · @app.get("/x") · #[Route('/x')] · [HttpGet("x")] route → the method below it
|
|
6
|
+
* callRoutes app.get('/x', h) · path('x/', views.h) · r.GET("/x", h) route → the handler named
|
|
7
|
+
* fsRoutes a page module whose path is its route route → the module
|
|
8
|
+
* tableMapping @Table(name = "t") · __tablename__ · $table · conventions model → table, migration → table
|
|
9
|
+
* templateRoots render('a/b.html') · render_template('x.html') · View("Name") code → the template file
|
|
10
|
+
*
|
|
11
|
+
* Every edge is emitted only when its target exists; what a rule cannot settle
|
|
12
|
+
* (a closure, a name built at runtime) gets no edge rather than a guess.
|
|
13
|
+
*/
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
|
|
16
|
+
import type { SourceFile } from '../../build.js';
|
|
17
|
+
import type { CodeNode } from '../../types.js';
|
|
18
|
+
import { type Kit, lineIndex } from './kit.js';
|
|
19
|
+
|
|
20
|
+
export function joinRoute(...parts: Array<string | undefined>): string {
|
|
21
|
+
const joined = parts
|
|
22
|
+
.filter((p): p is string => typeof p === 'string' && p.length > 0)
|
|
23
|
+
.map((p) => p.replace(/^\/+|\/+$/g, ''))
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.join('/');
|
|
26
|
+
return `/${joined}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AnnotationRouteSpec {
|
|
30
|
+
exts: readonly string[];
|
|
31
|
+
/** A route decorator; named groups `verb` (optional) and `path` (optional). */
|
|
32
|
+
route: RegExp;
|
|
33
|
+
/** HTTP verb when the decorator carries it in its name; `ANY` otherwise. */
|
|
34
|
+
verb?: (m: RegExpMatchArray) => string;
|
|
35
|
+
/** A class-level prefix decorator just above the class; named group `path`. */
|
|
36
|
+
prefix?: RegExp;
|
|
37
|
+
/** How far above a class its prefix decorator may sit, in lines. */
|
|
38
|
+
prefixReach?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function annotationRoutes(kit: Kit, spec: AnnotationRouteSpec): number {
|
|
42
|
+
let count = 0;
|
|
43
|
+
for (const f of kit.filesWithExt(...spec.exts)) {
|
|
44
|
+
const at = lineIndex(f.source);
|
|
45
|
+
const prefixes = spec.prefix
|
|
46
|
+
? [...f.source.matchAll(spec.prefix)].map((m) => ({ line: at(m.index as number), path: m.groups?.['path'] ?? '' }))
|
|
47
|
+
: [];
|
|
48
|
+
for (const m of f.source.matchAll(spec.route)) {
|
|
49
|
+
const index = m.index as number;
|
|
50
|
+
// A decorator inside a line comment is prose, not a route. A docblock
|
|
51
|
+
// line (` * @Route`) is not skipped: annotation-routed frameworks read it.
|
|
52
|
+
const lineStart = f.source.lastIndexOf('\n', index - 1) + 1;
|
|
53
|
+
if (/\/\/|^\s*#(?!\[)/.test(f.source.slice(lineStart, index))) continue;
|
|
54
|
+
const line = at(index);
|
|
55
|
+
const handler = kit.nextSymbol(f.path, line, ['method', 'function']);
|
|
56
|
+
const cls = handler ? kit.classAt(f.path, handler.source_location[0] as number) : null;
|
|
57
|
+
const reach = spec.prefixReach ?? 6;
|
|
58
|
+
const clsLine = cls ? (cls.source_location[0] as number) : -1;
|
|
59
|
+
const prefix = cls ? prefixes.filter((p) => p.line <= clsLine && p.line >= clsLine - reach).pop()?.path : undefined;
|
|
60
|
+
const verb = (spec.verb ? spec.verb(m) : (m.groups?.['verb'] ?? 'ANY')).toUpperCase();
|
|
61
|
+
const id = kit.route(f.path, verb, joinRoute(prefix, m.groups?.['path']), line);
|
|
62
|
+
if (handler) kit.emit(id, handler.id, 'routes-to', 'route-table');
|
|
63
|
+
count += 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return count;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface CallRouteSpec {
|
|
70
|
+
exts: readonly string[];
|
|
71
|
+
/**
|
|
72
|
+
* A route registration; named groups `verb` (optional), `path`, `rest` (the
|
|
73
|
+
* text after the path). Capture `rest` inside a lookahead —
|
|
74
|
+
* `(?=(?<rest>[\s\S]{0,300}))` — or it swallows the next registration.
|
|
75
|
+
*/
|
|
76
|
+
call: RegExp;
|
|
77
|
+
verb?: (m: RegExpMatchArray) => string;
|
|
78
|
+
/** Resolve the handler from `rest`; the default reads the last argument as a name or `obj.member`. */
|
|
79
|
+
handler?: (kit: Kit, file: string, rest: string) => string | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The last argument of a call's remaining text, if it is a plain name or `obj.member`. */
|
|
83
|
+
export function lastArgName(rest: string): string | null {
|
|
84
|
+
let depth = 0;
|
|
85
|
+
let end = rest.length;
|
|
86
|
+
for (let i = 0; i < rest.length; i++) {
|
|
87
|
+
const c = rest[i];
|
|
88
|
+
if (c === '(' || c === '[' || c === '{') depth += 1;
|
|
89
|
+
else if (c === ')' || c === ']' || c === '}') {
|
|
90
|
+
if (depth === 0) {
|
|
91
|
+
end = i;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
depth -= 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const args = rest.slice(0, end).split(',').map((a) => a.trim()).filter(Boolean);
|
|
98
|
+
const last = args[args.length - 1];
|
|
99
|
+
return last && /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?$/.test(last) ? last : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function defaultHandler(kit: Kit, file: string, rest: string): string | null {
|
|
103
|
+
const name = lastArgName(rest);
|
|
104
|
+
if (!name) return null;
|
|
105
|
+
const [head, member] = name.split('.') as [string, string | undefined];
|
|
106
|
+
const resolved = kit.resolveName(file, head);
|
|
107
|
+
if (!member) return resolved;
|
|
108
|
+
if (resolved) return kit.methodOf(resolved, member) ?? kit.resolveName(path.posix.join(path.posix.dirname(file), head), member);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function callRoutes(kit: Kit, spec: CallRouteSpec): number {
|
|
113
|
+
let count = 0;
|
|
114
|
+
for (const f of kit.filesWithExt(...spec.exts)) {
|
|
115
|
+
const at = lineIndex(f.source);
|
|
116
|
+
for (const m of f.source.matchAll(spec.call)) {
|
|
117
|
+
const line = at(m.index as number);
|
|
118
|
+
const verb = (spec.verb ? spec.verb(m) : (m.groups?.['verb'] ?? 'ANY')).toUpperCase();
|
|
119
|
+
const id = kit.route(f.path, verb, joinRoute(m.groups?.['path']), line);
|
|
120
|
+
const target = (spec.handler ?? defaultHandler)(kit, f.path, m.groups?.['rest'] ?? '');
|
|
121
|
+
if (target) kit.emit(id, target, 'routes-to', 'route-table');
|
|
122
|
+
count += 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return count;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface FsRouteSpec {
|
|
129
|
+
/** The route of a path, or null when it is no page module. */
|
|
130
|
+
route: (p: string) => { verb: string; path: string } | null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function fsRoutes(kit: Kit, spec: FsRouteSpec): number {
|
|
134
|
+
let count = 0;
|
|
135
|
+
for (const p of kit.paths) {
|
|
136
|
+
const r = spec.route(p);
|
|
137
|
+
if (!r) continue;
|
|
138
|
+
kit.fileNode(p);
|
|
139
|
+
const id = kit.route(p, r.verb, r.path, 1);
|
|
140
|
+
kit.emit(id, p, 'routes-to', 'route-table');
|
|
141
|
+
count += 1;
|
|
142
|
+
}
|
|
143
|
+
return count;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface TableMappingSpec {
|
|
147
|
+
exts: readonly string[];
|
|
148
|
+
/** Marks the class it precedes or opens as mapped to a table. */
|
|
149
|
+
entity: RegExp;
|
|
150
|
+
/** The explicit table name, searched in the entity's declaration region; named group `name`. */
|
|
151
|
+
tableName?: RegExp;
|
|
152
|
+
/** The table a mapped class gets when nothing names one. */
|
|
153
|
+
convention?: (className: string) => string;
|
|
154
|
+
/** A migration or schema statement creating a table; named group `name`. */
|
|
155
|
+
migration?: { files: (p: string) => boolean; create: RegExp };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function snakePlural(name: string): string {
|
|
159
|
+
const snake = name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
160
|
+
if (/[^aeiou]y$/.test(snake)) return `${snake.slice(0, -1)}ies`;
|
|
161
|
+
if (/(s|x|z|ch|sh)$/.test(snake)) return `${snake}es`;
|
|
162
|
+
return `${snake}s`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function tableMapping(kit: Kit, spec: TableMappingSpec): number {
|
|
166
|
+
let count = 0;
|
|
167
|
+
if (spec.migration) {
|
|
168
|
+
for (const f of kit.files.filter((x) => spec.migration?.files(x.path))) {
|
|
169
|
+
const at = lineIndex(f.source);
|
|
170
|
+
for (const m of f.source.matchAll(spec.migration.create)) {
|
|
171
|
+
const name = m.groups?.['name'];
|
|
172
|
+
if (!name) continue;
|
|
173
|
+
kit.emit(f.path, kit.table(name, f.path, at(m.index as number)), 'migrates', 'schema-call');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (const f of kit.filesWithExt(...spec.exts)) {
|
|
178
|
+
const at = lineIndex(f.source);
|
|
179
|
+
const lines = f.source.split('\n');
|
|
180
|
+
for (const m of f.source.matchAll(spec.entity)) {
|
|
181
|
+
const line = at(m.index as number);
|
|
182
|
+
const cls = kit.classAt(f.path, line) ?? kit.nextSymbol(f.path, line, ['class']);
|
|
183
|
+
if (!cls || cls.kind !== 'class') continue;
|
|
184
|
+
const start = cls.source_location[0] as number;
|
|
185
|
+
const end = (cls.source_location[2] ?? start) as number;
|
|
186
|
+
// Up to four lines of decorators above the class — but never past the
|
|
187
|
+
// end of the class before it, whose own table name would be read.
|
|
188
|
+
let floor = 0;
|
|
189
|
+
for (const n of kit.nodesInFile(f.path)) {
|
|
190
|
+
if (n.kind !== 'class' || n.id === cls.id) continue;
|
|
191
|
+
const nEnd = (n.source_location[2] ?? n.source_location[0]) as number;
|
|
192
|
+
if (nEnd < start && nEnd > floor) floor = nEnd;
|
|
193
|
+
}
|
|
194
|
+
const region = lines.slice(Math.max(floor, Math.min(line, start) - 1 - 4), end).join('\n');
|
|
195
|
+
const explicit = spec.tableName ? spec.tableName.exec(region)?.groups?.['name'] : undefined;
|
|
196
|
+
const name = explicit ?? spec.convention?.(cls.label);
|
|
197
|
+
if (!name) continue;
|
|
198
|
+
kit.emit(cls.id, kit.table(name, f.path, start), 'persists', 'schema-call');
|
|
199
|
+
count += 1;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return count;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface TemplateRootSpec {
|
|
206
|
+
exts: readonly string[];
|
|
207
|
+
/** A render call; named group `name` is the template as written. */
|
|
208
|
+
call: RegExp;
|
|
209
|
+
/** Directories templates live under, repo- or package-root-relative. */
|
|
210
|
+
roots: readonly string[];
|
|
211
|
+
/** Extensions tried when the name carries none. */
|
|
212
|
+
extensions: readonly string[];
|
|
213
|
+
/** Rewrite a name before lookup (`a.b` → `a/b`). */
|
|
214
|
+
normalize?: (name: string) => string;
|
|
215
|
+
/** Further template directories for one file, repo-relative (a folder its own source declares). */
|
|
216
|
+
rootsFor?: (file: SourceFile) => readonly string[];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function templateRoots(kit: Kit, spec: TemplateRootSpec): number {
|
|
220
|
+
const inventory = new Set(kit.paths);
|
|
221
|
+
let count = 0;
|
|
222
|
+
for (const f of kit.filesWithExt(...spec.exts)) {
|
|
223
|
+
const at = lineIndex(f.source);
|
|
224
|
+
for (const m of f.source.matchAll(spec.call)) {
|
|
225
|
+
const raw = m.groups?.['name'];
|
|
226
|
+
if (!raw) continue;
|
|
227
|
+
const name = spec.normalize ? spec.normalize(raw) : raw;
|
|
228
|
+
const hasExt = /\.[A-Za-z0-9]+$/.test(path.posix.basename(name));
|
|
229
|
+
const hits = new Set<string>();
|
|
230
|
+
const dirs = [...kit.roots.flatMap((r) => spec.roots.map((t) => path.posix.join(r, t))), ...(spec.rootsFor?.(f) ?? [])];
|
|
231
|
+
for (const root of new Set(dirs)) {
|
|
232
|
+
for (const c of hasExt ? [path.posix.join(root, name)] : spec.extensions.map((x) => path.posix.join(root, `${name}${x}`))) {
|
|
233
|
+
if (inventory.has(c)) hits.add(c);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (hits.size !== 1) continue;
|
|
237
|
+
const target = [...hits][0] as string;
|
|
238
|
+
kit.fileNode(target);
|
|
239
|
+
kit.emit(kit.enclosing(f.path, at(m.index as number)), target, 'renders', 'directive-scan');
|
|
240
|
+
count += 1;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return count;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** The class a node belongs to, for a method id `<file>#<Class>::<m>`. */
|
|
247
|
+
export function classOf(n: CodeNode): string {
|
|
248
|
+
return n.id.split('::')[0] as string;
|
|
249
|
+
}
|