@sdd-method/author-cli 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +17 -0
- package/README.md +6 -2
- package/dist/author-bin.js +8 -0
- package/dist/author-program.js +99 -0
- package/dist/lib/adr-origin.js +59 -0
- package/dist/lib/agent-harness.js +37 -0
- package/dist/lib/api-surface/__fixtures__/serverless-repo/serverless/serverless.yml +18 -0
- package/dist/lib/api-surface/analyzers/library.js +95 -0
- package/dist/lib/api-surface/analyzers/serverless.js +336 -0
- package/dist/lib/api-surface/analyzers/spring.js +237 -0
- package/dist/lib/api-surface/analyzers/terraform.js +109 -0
- package/dist/lib/api-surface/detect.js +25 -0
- package/dist/lib/api-surface/emit.js +109 -0
- package/dist/lib/api-surface/file-set.js +43 -0
- package/dist/lib/api-surface/index.js +134 -0
- package/dist/lib/api-surface/types.js +17 -0
- package/dist/lib/briefing/index.js +13 -0
- package/dist/lib/briefing/render.js +132 -0
- package/dist/lib/briefing/variables.js +92 -0
- package/dist/lib/catalogue/application-archetype/vocabulary.js +177 -0
- package/dist/lib/catalogue/archetype/maps/android.yaml +66 -0
- package/dist/lib/catalogue/archetype/maps/swiftui.yaml +58 -0
- package/dist/lib/catalogue/archetype/maps/uikit.yaml +48 -0
- package/dist/lib/catalogue/archetype/maps/web-generic.yaml +86 -0
- package/dist/lib/catalogue/archetype/openui-archetype-vocabulary.yaml +46 -0
- package/dist/lib/catalogue/archetype/resolver.js +103 -0
- package/dist/lib/catalogue/archetype/wireframe.js +418 -0
- package/dist/lib/catalogue/baseline-check.js +151 -0
- package/dist/lib/catalogue/build.js +1502 -0
- package/dist/lib/catalogue/builders/adr-supersedes.js +103 -0
- package/dist/lib/catalogue/builders/adrs.js +222 -0
- package/dist/lib/catalogue/builders/application-conformance-coverage.js +47 -0
- package/dist/lib/catalogue/builders/application-data-plane-bindings.js +115 -0
- package/dist/lib/catalogue/builders/c4-components.js +229 -0
- package/dist/lib/catalogue/builders/c4-containers.js +240 -0
- package/dist/lib/catalogue/builders/c4-entity-rollups.js +87 -0
- package/dist/lib/catalogue/builders/c4-external-systems.js +194 -0
- package/dist/lib/catalogue/builders/c4-l1-candidates.js +107 -0
- package/dist/lib/catalogue/builders/c4-systems.js +98 -0
- package/dist/lib/catalogue/builders/capabilities.js +488 -0
- package/dist/lib/catalogue/builders/capability-domain-model.js +74 -0
- package/dist/lib/catalogue/builders/capability-edges.js +256 -0
- package/dist/lib/catalogue/builders/capability-spec-walker.js +99 -0
- package/dist/lib/catalogue/builders/capability-spec.js +348 -0
- package/dist/lib/catalogue/builders/container-dependency.js +183 -0
- package/dist/lib/catalogue/builders/contracts.js +402 -0
- package/dist/lib/catalogue/builders/data-items.js +344 -0
- package/dist/lib/catalogue/builders/data-store-mapping.js +113 -0
- package/dist/lib/catalogue/builders/domain-models.js +175 -0
- package/dist/lib/catalogue/builders/domains.js +119 -0
- package/dist/lib/catalogue/builders/edge-label-merge.js +19 -0
- package/dist/lib/catalogue/builders/entities-manifest.js +160 -0
- package/dist/lib/catalogue/builders/entities-mermaid.js +189 -0
- package/dist/lib/catalogue/builders/entity-persistence.js +102 -0
- package/dist/lib/catalogue/builders/entity-slug.js +47 -0
- package/dist/lib/catalogue/builders/event-catalog-walker.js +186 -0
- package/dist/lib/catalogue/builders/event-edges.js +327 -0
- package/dist/lib/catalogue/builders/events.js +168 -0
- package/dist/lib/catalogue/builders/evidence-class.js +43 -0
- package/dist/lib/catalogue/builders/evidence-target-repo.js +67 -0
- package/dist/lib/catalogue/builders/external-system-exemption.js +120 -0
- package/dist/lib/catalogue/builders/feature-contract.js +77 -0
- package/dist/lib/catalogue/builders/feature-domain-model.js +50 -0
- package/dist/lib/catalogue/builders/feature-grounding-walker.js +254 -0
- package/dist/lib/catalogue/builders/feature-grounding.js +391 -0
- package/dist/lib/catalogue/builders/feature-groups.js +63 -0
- package/dist/lib/catalogue/builders/feature-subscription-channels.js +228 -0
- package/dist/lib/catalogue/builders/feature-tree-walker.js +295 -0
- package/dist/lib/catalogue/builders/features.js +300 -0
- package/dist/lib/catalogue/builders/glossary-xref.js +378 -0
- package/dist/lib/catalogue/builders/glossary.js +204 -0
- package/dist/lib/catalogue/builders/implementations.js +214 -0
- package/dist/lib/catalogue/builders/index.js +64 -0
- package/dist/lib/catalogue/builders/integration-components.js +344 -0
- package/dist/lib/catalogue/builders/integrations.js +181 -0
- package/dist/lib/catalogue/builders/persona-c4system.js +149 -0
- package/dist/lib/catalogue/builders/persona-container.js +133 -0
- package/dist/lib/catalogue/builders/persona-journey-edges.js +133 -0
- package/dist/lib/catalogue/builders/personas.js +152 -0
- package/dist/lib/catalogue/builders/product-edges.js +106 -0
- package/dist/lib/catalogue/builders/product-platform-dependencies.js +405 -0
- package/dist/lib/catalogue/builders/products.js +162 -0
- package/dist/lib/catalogue/builders/quality-scores.js +635 -0
- package/dist/lib/catalogue/builders/repo-capability-exemption.js +61 -0
- package/dist/lib/catalogue/builders/repo-dependency.js +127 -0
- package/dist/lib/catalogue/builders/repo-edges.js +340 -0
- package/dist/lib/catalogue/builders/repositories.js +458 -0
- package/dist/lib/catalogue/builders/scaffolds.js +197 -0
- package/dist/lib/catalogue/builders/screen-tree.js +222 -0
- package/dist/lib/catalogue/builders/screens.js +396 -0
- package/dist/lib/catalogue/builders/service-capability-exemption.js +63 -0
- package/dist/lib/catalogue/builders/service-contract-consumes.js +186 -0
- package/dist/lib/catalogue/builders/service-contract.js +74 -0
- package/dist/lib/catalogue/builders/service-entity-tables.js +187 -0
- package/dist/lib/catalogue/builders/services.js +232 -0
- package/dist/lib/catalogue/builders/system-external-dependency.js +115 -0
- package/dist/lib/catalogue/builders/user-journeys.js +253 -0
- package/dist/lib/catalogue/canonical-schema.yaml +4117 -0
- package/dist/lib/catalogue/cli-version.js +10 -0
- package/dist/lib/catalogue/contract-id.js +49 -0
- package/dist/lib/catalogue/coverage-predicates.js +603 -0
- package/dist/lib/catalogue/coverage-render.js +552 -0
- package/dist/lib/catalogue/coverage-run.js +115 -0
- package/dist/lib/catalogue/csv-reader.js +97 -0
- package/dist/lib/catalogue/csv-writer.js +96 -0
- package/dist/lib/catalogue/data-plane-coherence.js +77 -0
- package/dist/lib/catalogue/ddl-generator.js +233 -0
- package/dist/lib/catalogue/dedupe-keyed-tables.js +0 -0
- package/dist/lib/catalogue/engine-validator.js +239 -0
- package/dist/lib/catalogue/ext-id.js +76 -0
- package/dist/lib/catalogue/feature-grounding-aliases-loader.js +65 -0
- package/dist/lib/catalogue/feature-repo-zero-resolution-summary.js +31 -0
- package/dist/lib/catalogue/finding-identity.js +183 -0
- package/dist/lib/catalogue/findings.js +46 -0
- package/dist/lib/catalogue/fk-validator.js +136 -0
- package/dist/lib/catalogue/freshness.js +70 -0
- package/dist/lib/catalogue/gate-checks.js +338 -0
- package/dist/lib/catalogue/graph-projection.js +346 -0
- package/dist/lib/catalogue/impact-run.js +228 -0
- package/dist/lib/catalogue/manifest-loader.js +435 -0
- package/dist/lib/catalogue/platform-scope.js +7 -0
- package/dist/lib/catalogue/population-checks.js +87 -0
- package/dist/lib/catalogue/relationship-semantics.js +66 -0
- package/dist/lib/catalogue/reverse-product-quality.js +99 -0
- package/dist/lib/catalogue/rubric-coverage-map.yaml +871 -0
- package/dist/lib/catalogue/runway-vocabulary-loader.js +56 -0
- package/dist/lib/catalogue/scaffold-delta.js +261 -0
- package/dist/lib/catalogue/schema-compat.js +52 -0
- package/dist/lib/catalogue/schema-loader.js +87 -0
- package/dist/lib/catalogue/scores-coverage.js +573 -0
- package/dist/lib/catalogue/screen-validation-loader.js +89 -0
- package/dist/lib/catalogue/service-architecture-parser.js +193 -0
- package/dist/lib/catalogue/service-contract-aliases-loader.js +109 -0
- package/dist/lib/catalogue/staleness-verdicts.js +345 -0
- package/dist/lib/catalogue/traceability-loader.js +350 -0
- package/dist/lib/catalogue/validate.js +200 -0
- package/dist/lib/catalogue/version.js +18 -0
- package/dist/lib/conformance/binding.js +70 -0
- package/dist/lib/conformance/finding.js +69 -0
- package/dist/lib/conformance/index.js +578 -0
- package/dist/lib/context-pull/index.js +191 -0
- package/dist/lib/context-pull/resolve.js +70 -0
- package/dist/lib/context-pull/retired-mcp-setup.js +7 -0
- package/dist/lib/contracts-availability/index.js +267 -0
- package/dist/lib/generate-briefing/index.js +77 -0
- package/dist/lib/list/discover.js +100 -0
- package/dist/lib/list/format.js +84 -0
- package/dist/lib/list/index.js +16 -0
- package/dist/lib/lock-file.js +437 -0
- package/dist/lib/pack/log.js +18 -0
- package/dist/lib/pack/schema.js +121 -0
- package/dist/lib/pack/seed.js +182 -0
- package/dist/lib/pack/validate.js +49 -0
- package/dist/lib/package-version.js +92 -0
- package/dist/lib/plan/index.js +69 -0
- package/dist/lib/plan/read-sdd.js +102 -0
- package/dist/lib/plan/render-template.js +84 -0
- package/dist/lib/repo-kind.js +129 -0
- package/dist/lib/scaffold/domain.js +234 -0
- package/dist/lib/scaffold/registry.js +136 -0
- package/dist/lib/scripts.js +7 -0
- package/dist/lib/sdd-index/repo-config.js +245 -0
- package/dist/lib/sdd-reader.js +91 -0
- package/dist/lib/snapshot/advance.js +454 -0
- package/dist/lib/snapshot/diff.js +190 -0
- package/dist/lib/snapshot/fold.js +208 -0
- package/dist/lib/snapshot/land.js +178 -0
- package/dist/lib/snapshot/manifest.js +206 -0
- package/dist/lib/snapshot/targets.js +169 -0
- package/dist/lib/snapshot/verify.js +91 -0
- package/dist/lib/sync/briefing-merge.js +106 -0
- package/dist/lib/sync/bundle-cache.js +109 -0
- package/dist/lib/sync/yaml-block-merge.js +120 -0
- package/dist/lib/validate/aggregate.js +51 -0
- package/dist/lib/validate/bundle-coherence.js +74 -0
- package/dist/lib/validate/dispatch.js +243 -0
- package/dist/lib/validate/index.js +110 -0
- package/dist/lib/validate/plan.js +54 -0
- package/dist/lib/validate/runner.js +85 -0
- package/dist/lib/validate/yaml-parse.js +112 -0
- package/dist/maintain-version.json +4 -0
- package/dist/verbs/api-surface.js +71 -0
- package/dist/verbs/catalogue-check.js +28 -0
- package/dist/verbs/catalogue-verify.js +114 -0
- package/dist/verbs/conformance.js +34 -0
- package/dist/verbs/context.js +39 -0
- package/dist/verbs/contracts.js +34 -0
- package/dist/verbs/generate-briefing.js +29 -0
- package/dist/verbs/list.js +45 -0
- package/dist/verbs/pack-validate.js +17 -0
- package/dist/verbs/path-option.js +25 -0
- package/dist/verbs/plan.js +42 -0
- package/dist/verbs/render.js +87 -0
- package/dist/verbs/scaffold.js +83 -0
- package/dist/verbs/snapshot.js +134 -0
- package/dist/verbs/validate.js +48 -0
- package/dist/verify-program.js +122 -0
- package/dist/verify-version.json +4 -0
- package/package.json +32 -4
- package/scripts/validate-app-dependencies.sh +664 -0
- package/scripts/validate-app-docs.sh +199 -0
- package/scripts/validate-integration-docs.sh +505 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { parse as parseYaml } from "yaml";
|
|
2
|
+
import { allBySuffix } from "../file-set.js";
|
|
3
|
+
// node-serverless analyzer.
|
|
4
|
+
//
|
|
5
|
+
// The serverless.yml in these repos is Go/Helm-templated (`{{ ... }}`, `${self:...}`, `${opt:...}`).
|
|
6
|
+
// Strategy: sanitise the template directives so the YAML parser succeeds, then read functions/events
|
|
7
|
+
// /provider/resources structurally. If the sanitised doc still won't parse, fall back to a line-based
|
|
8
|
+
// regex sweep so we never come back empty on a real (messy) file.
|
|
9
|
+
export async function analyzeServerless(fileSet) {
|
|
10
|
+
const serverlessFiles = fileSet.files.filter((f) => {
|
|
11
|
+
const lower = f.toLowerCase();
|
|
12
|
+
return lower.endsWith("serverless.yml") || lower.endsWith("serverless.yaml");
|
|
13
|
+
});
|
|
14
|
+
const endpoints = [];
|
|
15
|
+
const downstream = [];
|
|
16
|
+
const sharedInfra = new Set();
|
|
17
|
+
const externalContracts = new Set();
|
|
18
|
+
let runtime = "";
|
|
19
|
+
for (const slPath of serverlessFiles) {
|
|
20
|
+
const raw = await fileSet.read(slPath);
|
|
21
|
+
if (raw === "")
|
|
22
|
+
continue;
|
|
23
|
+
const sanitised = sanitiseTemplate(raw);
|
|
24
|
+
let doc = null;
|
|
25
|
+
try {
|
|
26
|
+
doc = parseYaml(sanitised);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
doc = null;
|
|
30
|
+
}
|
|
31
|
+
if (isRecord(doc)) {
|
|
32
|
+
runtime = runtime || extractRuntime(doc);
|
|
33
|
+
collectFromStructured(doc, endpoints, downstream, sharedInfra);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
// Structured parse failed — fall back to regex extraction over the raw text.
|
|
37
|
+
collectFromRegex(raw, endpoints, downstream, sharedInfra);
|
|
38
|
+
runtime = runtime || (raw.match(/^\s*runtime:\s*([\w.-]+)/m)?.[1] ?? "");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Scan handler source for AWS SDK clients + outbound HTTP base URLs → downstream deps.
|
|
42
|
+
await scanHandlerSources(fileSet, downstream, externalContracts);
|
|
43
|
+
dedupeDownstream(downstream);
|
|
44
|
+
return {
|
|
45
|
+
stack: "node-serverless",
|
|
46
|
+
component_type: "service",
|
|
47
|
+
endpoints,
|
|
48
|
+
dependencies: { downstream, upstream: [] },
|
|
49
|
+
external_contracts: [...externalContracts].sort(),
|
|
50
|
+
deployment: { runtime, shared_infra: [...sharedInfra].sort() },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Make a Helm/Go-templated serverless.yml parseable as YAML:
|
|
55
|
+
* - drop whole control lines (`{{- if ... }}`, `{{ range ... }}`, `{{ end }}`) — they're block
|
|
56
|
+
* directives, not values, so removing the line keeps the surrounding structure valid;
|
|
57
|
+
* - replace inline `{{ expr }}` with a quoted placeholder so a value position stays a scalar;
|
|
58
|
+
* - leave `${self:...}` / `${opt:...}` serverless vars alone (they're valid YAML scalars already,
|
|
59
|
+
* just quote bare ones that would otherwise be read as a mapping).
|
|
60
|
+
*/
|
|
61
|
+
export function sanitiseTemplate(raw) {
|
|
62
|
+
const out = [];
|
|
63
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
64
|
+
const trimmed = line.trim();
|
|
65
|
+
// A line whose entire content is one template directive (control flow `{{ if }}`/`{{ end }}`,
|
|
66
|
+
// an assignment `{{ $x := ... }}`, or a bare expression `{{ .Values.X }}`) is a directive, not a
|
|
67
|
+
// mapping/value — drop the whole line so the surrounding YAML structure stays valid. Detected by:
|
|
68
|
+
// starts with `{{`/`{{-` and ends with `}}`/`-}}` with no YAML payload before/after the braces.
|
|
69
|
+
if (/^\{\{-?[\s\S]*?-?\}\}$/.test(trimmed)) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
// Inline `{{ ... }}` in a value position → placeholder scalar.
|
|
73
|
+
const replaced = line.replace(/\{\{-?.*?-?\}\}/g, "TEMPLATED");
|
|
74
|
+
out.push(replaced);
|
|
75
|
+
}
|
|
76
|
+
return out.join("\n");
|
|
77
|
+
}
|
|
78
|
+
function extractRuntime(doc) {
|
|
79
|
+
const provider = doc["provider"];
|
|
80
|
+
if (isRecord(provider) && typeof provider["runtime"] === "string") {
|
|
81
|
+
return provider["runtime"];
|
|
82
|
+
}
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
function collectFromStructured(doc, endpoints, downstream, sharedInfra) {
|
|
86
|
+
const functions = doc["functions"];
|
|
87
|
+
if (isRecord(functions)) {
|
|
88
|
+
for (const [fnName, fnDef] of Object.entries(functions)) {
|
|
89
|
+
if (!isRecord(fnDef))
|
|
90
|
+
continue;
|
|
91
|
+
const handler = typeof fnDef["handler"] === "string" ? fnDef["handler"] : fnName;
|
|
92
|
+
const events = fnDef["events"];
|
|
93
|
+
if (!Array.isArray(events))
|
|
94
|
+
continue;
|
|
95
|
+
for (const evt of events) {
|
|
96
|
+
if (!isRecord(evt))
|
|
97
|
+
continue;
|
|
98
|
+
const httpEvt = evt["http"] ?? evt["httpApi"];
|
|
99
|
+
if (httpEvt !== undefined) {
|
|
100
|
+
endpoints.push(httpEndpoint(httpEvt, handler));
|
|
101
|
+
}
|
|
102
|
+
// Non-http event sources (sqs/sns/eventBridge) are dependency signals, not endpoints.
|
|
103
|
+
collectEventSource(evt, fnName, downstream, sharedInfra);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// provider.iam + resources + environment → infra + downstream.
|
|
108
|
+
const provider = doc["provider"];
|
|
109
|
+
if (isRecord(provider)) {
|
|
110
|
+
collectInfraFromNode(provider, downstream, sharedInfra);
|
|
111
|
+
}
|
|
112
|
+
const resources = doc["resources"];
|
|
113
|
+
if (resources !== undefined) {
|
|
114
|
+
collectInfraFromNode(resources, downstream, sharedInfra);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function httpEndpoint(httpEvt, handler) {
|
|
118
|
+
let method = "ANY";
|
|
119
|
+
let path = "/";
|
|
120
|
+
let auth = "none";
|
|
121
|
+
if (typeof httpEvt === "string") {
|
|
122
|
+
// `http: GET users/list` shorthand.
|
|
123
|
+
const parts = httpEvt.trim().split(/\s+/);
|
|
124
|
+
if (parts.length >= 2) {
|
|
125
|
+
method = parts[0].toUpperCase();
|
|
126
|
+
path = normalisePath(parts[1]);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
path = normalisePath(httpEvt);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else if (isRecord(httpEvt)) {
|
|
133
|
+
if (typeof httpEvt["method"] === "string")
|
|
134
|
+
method = httpEvt["method"].toUpperCase();
|
|
135
|
+
if (typeof httpEvt["path"] === "string")
|
|
136
|
+
path = normalisePath(httpEvt["path"]);
|
|
137
|
+
const authorizer = httpEvt["authorizer"];
|
|
138
|
+
if (typeof authorizer === "string")
|
|
139
|
+
auth = `authorizer:${authorizer}`;
|
|
140
|
+
else if (isRecord(authorizer)) {
|
|
141
|
+
const name = authorizer["name"] ?? authorizer["type"] ?? authorizer["arn"];
|
|
142
|
+
auth = typeof name === "string" ? `authorizer:${name}` : "authorizer:custom";
|
|
143
|
+
}
|
|
144
|
+
else if (httpEvt["private"] === true) {
|
|
145
|
+
auth = "api-key";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { method, path, auth, handler };
|
|
149
|
+
}
|
|
150
|
+
function collectEventSource(evt, fnName, downstream, sharedInfra) {
|
|
151
|
+
if ("sqs" in evt) {
|
|
152
|
+
const detail = describeArnRef(evt["sqs"]) || `${fnName}-sqs`;
|
|
153
|
+
downstream.push({ target: detail, kind: "sqs", detail, load_bearing: true });
|
|
154
|
+
if (detail)
|
|
155
|
+
sharedInfra.add(detail);
|
|
156
|
+
}
|
|
157
|
+
if ("sns" in evt) {
|
|
158
|
+
const detail = describeArnRef(evt["sns"]) || `${fnName}-sns`;
|
|
159
|
+
downstream.push({ target: detail, kind: "sns", detail, load_bearing: true });
|
|
160
|
+
if (detail)
|
|
161
|
+
sharedInfra.add(detail);
|
|
162
|
+
}
|
|
163
|
+
if ("stream" in evt) {
|
|
164
|
+
const detail = describeArnRef(evt["stream"]) || `${fnName}-stream`;
|
|
165
|
+
downstream.push({ target: detail, kind: "dynamodb", detail, load_bearing: true });
|
|
166
|
+
}
|
|
167
|
+
if ("eventBridge" in evt) {
|
|
168
|
+
const eb = evt["eventBridge"];
|
|
169
|
+
const detail = isRecord(eb) && typeof eb["name"] === "string" ? eb["name"] : `${fnName}-eb`;
|
|
170
|
+
downstream.push({ target: detail, kind: "eventbridge", detail, load_bearing: false });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** Recursively collect DynamoDB tables, SQS/SNS refs, S3 buckets, VPC from a provider/resources node. */
|
|
174
|
+
function collectInfraFromNode(node, downstream, sharedInfra) {
|
|
175
|
+
walk(node, (key, value) => {
|
|
176
|
+
if (!isRecord(value))
|
|
177
|
+
return;
|
|
178
|
+
const type = value["Type"];
|
|
179
|
+
if (typeof type === "string") {
|
|
180
|
+
const props = isRecord(value["Properties"]) ? value["Properties"] : {};
|
|
181
|
+
if (type === "AWS::SQS::Queue") {
|
|
182
|
+
const name = strProp(props, "QueueName") || key;
|
|
183
|
+
downstream.push({ target: name, kind: "sqs", detail: name, load_bearing: true });
|
|
184
|
+
sharedInfra.add(name);
|
|
185
|
+
}
|
|
186
|
+
else if (type === "AWS::SNS::Topic") {
|
|
187
|
+
const name = strProp(props, "TopicName") || key;
|
|
188
|
+
downstream.push({ target: name, kind: "sns", detail: name, load_bearing: true });
|
|
189
|
+
sharedInfra.add(name);
|
|
190
|
+
}
|
|
191
|
+
else if (type === "AWS::DynamoDB::Table") {
|
|
192
|
+
const name = strProp(props, "TableName") || key;
|
|
193
|
+
downstream.push({ target: name, kind: "dynamodb", detail: name, load_bearing: true });
|
|
194
|
+
sharedInfra.add(name);
|
|
195
|
+
}
|
|
196
|
+
else if (type === "AWS::S3::Bucket") {
|
|
197
|
+
const name = strProp(props, "BucketName") || key;
|
|
198
|
+
downstream.push({ target: name, kind: "s3", detail: name, load_bearing: false });
|
|
199
|
+
sharedInfra.add(name);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function strProp(props, key) {
|
|
205
|
+
const v = props[key];
|
|
206
|
+
return typeof v === "string" ? v : "";
|
|
207
|
+
}
|
|
208
|
+
/** Visit every (key,value) pair in a nested object/array tree. */
|
|
209
|
+
function walk(node, visit) {
|
|
210
|
+
if (Array.isArray(node)) {
|
|
211
|
+
for (const el of node)
|
|
212
|
+
walk(el, visit);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (isRecord(node)) {
|
|
216
|
+
for (const [k, v] of Object.entries(node)) {
|
|
217
|
+
visit(k, v);
|
|
218
|
+
walk(v, visit);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function describeArnRef(node) {
|
|
223
|
+
if (typeof node === "string")
|
|
224
|
+
return node;
|
|
225
|
+
if (isRecord(node)) {
|
|
226
|
+
const arn = node["arn"];
|
|
227
|
+
if (typeof arn === "string")
|
|
228
|
+
return arn;
|
|
229
|
+
if (isRecord(arn)) {
|
|
230
|
+
const getAtt = arn["Fn::GetAtt"];
|
|
231
|
+
if (Array.isArray(getAtt) && typeof getAtt[0] === "string")
|
|
232
|
+
return getAtt[0];
|
|
233
|
+
}
|
|
234
|
+
const ref = node["Ref"];
|
|
235
|
+
if (typeof ref === "string")
|
|
236
|
+
return ref;
|
|
237
|
+
}
|
|
238
|
+
return "";
|
|
239
|
+
}
|
|
240
|
+
// --- regex fallback (when YAML won't parse even after sanitising) ---
|
|
241
|
+
function collectFromRegex(raw, endpoints, downstream, sharedInfra) {
|
|
242
|
+
const lines = raw.split(/\r?\n/);
|
|
243
|
+
let currentHandler = "";
|
|
244
|
+
for (let i = 0; i < lines.length; i++) {
|
|
245
|
+
const line = lines[i];
|
|
246
|
+
const handlerM = line.match(/^\s*handler:\s*([\w.\/-]+)/);
|
|
247
|
+
if (handlerM)
|
|
248
|
+
currentHandler = handlerM[1];
|
|
249
|
+
const methodM = line.match(/^\s*method:\s*([A-Za-z]+)/);
|
|
250
|
+
if (methodM) {
|
|
251
|
+
// Look a few lines around for a path:.
|
|
252
|
+
const path = findNearby(lines, i, /^\s*path:\s*([^\s]+)/) ?? "/";
|
|
253
|
+
const auth = findNearby(lines, i, /^\s*authorizer:\s*([\w-]+)/) ?? "none";
|
|
254
|
+
endpoints.push({
|
|
255
|
+
method: methodM[1].toUpperCase(),
|
|
256
|
+
path: normalisePath(path),
|
|
257
|
+
auth: auth === "none" ? "none" : `authorizer:${auth}`,
|
|
258
|
+
handler: currentHandler || "unknown",
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
const sqsM = line.match(/QueueName:\s*['"]?([\w${}:.\-]+)/);
|
|
262
|
+
if (sqsM) {
|
|
263
|
+
downstream.push({ target: sqsM[1], kind: "sqs", detail: sqsM[1], load_bearing: true });
|
|
264
|
+
sharedInfra.add(sqsM[1]);
|
|
265
|
+
}
|
|
266
|
+
const tableM = line.match(/TableName:\s*['"]?([\w${}:.\-]+)/);
|
|
267
|
+
if (tableM) {
|
|
268
|
+
downstream.push({ target: tableM[1], kind: "dynamodb", detail: tableM[1], load_bearing: true });
|
|
269
|
+
sharedInfra.add(tableM[1]);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function findNearby(lines, idx, re) {
|
|
274
|
+
for (let j = Math.max(0, idx - 3); j <= Math.min(lines.length - 1, idx + 3); j++) {
|
|
275
|
+
const m = lines[j].match(re);
|
|
276
|
+
if (m)
|
|
277
|
+
return m[1];
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
// --- handler source scan ---
|
|
282
|
+
const AWS_CLIENT_RE = [
|
|
283
|
+
{ re: /\bnew\s+(?:AWS\.)?DynamoDB(?:Client|\.DocumentClient)?\b|DynamoDbClient\b/, kind: "dynamodb" },
|
|
284
|
+
{ re: /\bnew\s+(?:AWS\.)?SQS\b|SqsClient\b|AmazonSQS\b/, kind: "sqs" },
|
|
285
|
+
{ re: /\bnew\s+(?:AWS\.)?SNS\b|SnsClient\b|AmazonSNS\b/, kind: "sns" },
|
|
286
|
+
{ re: /\bnew\s+(?:AWS\.)?S3\b|S3Client\b|AmazonS3\b/, kind: "s3" },
|
|
287
|
+
{ re: /SecretsManager(?:Client)?\b|AWSSecretsManager\b/, kind: "secretsmanager" },
|
|
288
|
+
];
|
|
289
|
+
async function scanHandlerSources(fileSet, downstream, externalContracts) {
|
|
290
|
+
const sources = [
|
|
291
|
+
...allBySuffix(fileSet.files, ".js"),
|
|
292
|
+
...allBySuffix(fileSet.files, ".ts"),
|
|
293
|
+
...allBySuffix(fileSet.files, ".java"),
|
|
294
|
+
].filter((f) => !/(^|\/)(node_modules|target|dist|build)\//.test(f) && !/\.test\.|Test\.java$/.test(f));
|
|
295
|
+
for (const path of sources) {
|
|
296
|
+
const src = await fileSet.read(path);
|
|
297
|
+
if (src === "")
|
|
298
|
+
continue;
|
|
299
|
+
for (const { re, kind } of AWS_CLIENT_RE) {
|
|
300
|
+
if (re.test(src)) {
|
|
301
|
+
downstream.push({ target: kind, kind, detail: `client in ${path}`, load_bearing: false });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// Outbound HTTP base URLs.
|
|
305
|
+
for (const m of src.matchAll(/axios\.create\(\s*\{\s*baseURL:\s*['"`]([^'"`]+)/g)) {
|
|
306
|
+
externalContracts.add(m[1]);
|
|
307
|
+
downstream.push({ target: m[1], kind: "http", detail: m[1], load_bearing: false });
|
|
308
|
+
}
|
|
309
|
+
for (const m of src.matchAll(/fetch\(\s*['"`](https?:\/\/[^'"`]+)/g)) {
|
|
310
|
+
externalContracts.add(m[1]);
|
|
311
|
+
downstream.push({ target: m[1], kind: "http", detail: m[1], load_bearing: false });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function dedupeDownstream(deps) {
|
|
316
|
+
const seen = new Set();
|
|
317
|
+
for (let i = deps.length - 1; i >= 0; i--) {
|
|
318
|
+
const dep = deps[i];
|
|
319
|
+
const key = `${dep.kind}|${dep.detail}`;
|
|
320
|
+
if (seen.has(key))
|
|
321
|
+
deps.splice(i, 1);
|
|
322
|
+
else
|
|
323
|
+
seen.add(key);
|
|
324
|
+
}
|
|
325
|
+
deps.sort((a, b) => (a.kind + a.detail).localeCompare(b.kind + b.detail));
|
|
326
|
+
}
|
|
327
|
+
function normalisePath(p) {
|
|
328
|
+
let s = p.trim().replace(/^['"]|['"]$/g, "");
|
|
329
|
+
if (!s.startsWith("/"))
|
|
330
|
+
s = `/${s}`;
|
|
331
|
+
return s;
|
|
332
|
+
}
|
|
333
|
+
function isRecord(value) {
|
|
334
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
335
|
+
}
|
|
336
|
+
//# sourceMappingURL=serverless.js.map
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { allBySuffix, findBySuffix } from "../file-set.js";
|
|
2
|
+
// java-spring analyzer. Scans **/*.java for Spring-web controllers and outbound dependencies, and
|
|
3
|
+
// pom.xml / build.gradle for the Java runtime version. application.yml/properties → shared_infra.
|
|
4
|
+
const HTTP_METHOD_BY_ANNOTATION = {
|
|
5
|
+
GetMapping: "GET",
|
|
6
|
+
PostMapping: "POST",
|
|
7
|
+
PutMapping: "PUT",
|
|
8
|
+
DeleteMapping: "DELETE",
|
|
9
|
+
PatchMapping: "PATCH",
|
|
10
|
+
};
|
|
11
|
+
export async function analyzeSpring(fileSet) {
|
|
12
|
+
const javaFiles = allBySuffix(fileSet.files, ".java").filter((f) => !/(^|\/)(target|build)\//.test(f) && !/Test\.java$/.test(f));
|
|
13
|
+
const endpoints = [];
|
|
14
|
+
const downstream = [];
|
|
15
|
+
const sharedInfra = new Set();
|
|
16
|
+
const externalContracts = new Set();
|
|
17
|
+
for (const path of javaFiles) {
|
|
18
|
+
const src = await fileSet.read(path);
|
|
19
|
+
if (src === "")
|
|
20
|
+
continue;
|
|
21
|
+
collectEndpoints(src, path, endpoints);
|
|
22
|
+
collectDownstream(src, downstream, sharedInfra, externalContracts);
|
|
23
|
+
}
|
|
24
|
+
// Datasource / queue / topic names from config.
|
|
25
|
+
await collectConfigInfra(fileSet, sharedInfra);
|
|
26
|
+
const runtime = await detectJavaRuntime(fileSet);
|
|
27
|
+
dedupeDownstream(downstream);
|
|
28
|
+
endpoints.sort((a, b) => (a.path + a.method).localeCompare(b.path + b.method));
|
|
29
|
+
return {
|
|
30
|
+
stack: "java-spring",
|
|
31
|
+
component_type: "service",
|
|
32
|
+
endpoints,
|
|
33
|
+
dependencies: { downstream, upstream: [] },
|
|
34
|
+
external_contracts: [...externalContracts].sort(),
|
|
35
|
+
deployment: { runtime, shared_infra: [...sharedInfra].sort() },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function collectEndpoints(src, path, endpoints) {
|
|
39
|
+
const isController = /@(Rest)?Controller\b/.test(src);
|
|
40
|
+
if (!isController)
|
|
41
|
+
return;
|
|
42
|
+
const classBase = extractClassRequestMapping(src);
|
|
43
|
+
const classAuth = extractAuth(classLevelSlice(src));
|
|
44
|
+
// Walk method-mapping annotations. We pair each mapping annotation with the auth annotation(s)
|
|
45
|
+
// immediately preceding the method (within a small look-back window). The class-level
|
|
46
|
+
// @RequestMapping (which sits before the `class`/`interface` declaration) is the path prefix, not a
|
|
47
|
+
// method mapping — skip any mapping annotation that appears before the type declaration line.
|
|
48
|
+
const lines = src.split(/\r?\n/);
|
|
49
|
+
const typeDeclLine = lines.findIndex((l) => /\b(class|interface|enum)\s+\w+/.test(l));
|
|
50
|
+
for (let i = 0; i < lines.length; i++) {
|
|
51
|
+
if (typeDeclLine >= 0 && i < typeDeclLine)
|
|
52
|
+
continue;
|
|
53
|
+
const line = lines[i];
|
|
54
|
+
// @GetMapping("/x") / @PostMapping(value="/x") / @RequestMapping(value="/x", method=...).
|
|
55
|
+
const mappingM = line.match(/@(Get|Post|Put|Delete|Patch|Request)Mapping\b\s*(?:\(([^)]*)\))?/);
|
|
56
|
+
if (!mappingM)
|
|
57
|
+
continue;
|
|
58
|
+
const annotation = `${mappingM[1]}Mapping`;
|
|
59
|
+
const args = mappingM[2] ?? "";
|
|
60
|
+
let method = HTTP_METHOD_BY_ANNOTATION[annotation] ?? "";
|
|
61
|
+
if (annotation === "RequestMapping") {
|
|
62
|
+
const methodM = args.match(/method\s*=\s*(?:RequestMethod\.)?([A-Z]+)/);
|
|
63
|
+
method = methodM ? methodM[1] : "GET";
|
|
64
|
+
}
|
|
65
|
+
if (method === "")
|
|
66
|
+
method = "GET";
|
|
67
|
+
const subPath = extractPathArg(args);
|
|
68
|
+
const fullPath = joinPaths(classBase, subPath);
|
|
69
|
+
const methodAuth = extractAuth(lookBack(lines, i, 4)) || classAuth || "none";
|
|
70
|
+
endpoints.push({ method, path: fullPath, auth: methodAuth, handler: `${baseName(path)}` });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function classLevelSlice(src) {
|
|
74
|
+
// The annotations between the class's RequestMapping and the first method — best-effort: take the
|
|
75
|
+
// text up to the first mapping method annotation.
|
|
76
|
+
const idx = src.search(/@(Get|Post|Put|Delete|Patch)Mapping\b/);
|
|
77
|
+
return idx >= 0 ? src.slice(0, idx) : src;
|
|
78
|
+
}
|
|
79
|
+
function extractClassRequestMapping(src) {
|
|
80
|
+
// Class-level @RequestMapping("/x") sits before the class body's method mappings.
|
|
81
|
+
const m = src.match(/@RequestMapping\s*\(\s*([^)]*)\)/);
|
|
82
|
+
if (!m)
|
|
83
|
+
return "";
|
|
84
|
+
// Make sure this is the class-level one (appears before any method mapping).
|
|
85
|
+
const firstMethodMapping = src.search(/@(Get|Post|Put|Delete|Patch)Mapping\b/);
|
|
86
|
+
if (firstMethodMapping >= 0 && (m.index ?? 0) > firstMethodMapping)
|
|
87
|
+
return "";
|
|
88
|
+
return extractPathArg(m[1] ?? "");
|
|
89
|
+
}
|
|
90
|
+
function extractPathArg(args) {
|
|
91
|
+
// `"/events"` | `value = "/events"` | `path = "/events"` | `{"/a","/b"}` → first string literal.
|
|
92
|
+
const m = args.match(/(?:value|path)?\s*=?\s*\{?\s*"([^"]*)"/);
|
|
93
|
+
return m ? m[1] : "";
|
|
94
|
+
}
|
|
95
|
+
function extractAuth(text) {
|
|
96
|
+
const pre = text.match(/@PreAuthorize\s*\(\s*"([^"]+)"/);
|
|
97
|
+
if (pre)
|
|
98
|
+
return `preauthorize:${pre[1]}`;
|
|
99
|
+
const secured = text.match(/@Secured\s*\(\s*\{?\s*"?([^")}]+)/);
|
|
100
|
+
if (secured)
|
|
101
|
+
return `secured:${secured[1].trim()}`;
|
|
102
|
+
const roles = text.match(/@RolesAllowed\s*\(\s*\{?\s*"?([^")}]+)/);
|
|
103
|
+
if (roles)
|
|
104
|
+
return `roles:${roles[1].trim()}`;
|
|
105
|
+
return "";
|
|
106
|
+
}
|
|
107
|
+
function collectDownstream(src, downstream, sharedInfra, externalContracts) {
|
|
108
|
+
// @FeignClient(name = "x", url = "...") → feign.
|
|
109
|
+
for (const m of src.matchAll(/@FeignClient\s*\(([^)]*)\)/g)) {
|
|
110
|
+
const args = m[1] ?? "";
|
|
111
|
+
const name = args.match(/name\s*=\s*"([^"]+)"/)?.[1] ?? "feign-client";
|
|
112
|
+
const url = args.match(/url\s*=\s*"([^"]+)"/)?.[1];
|
|
113
|
+
downstream.push({ target: name, kind: "feign", detail: url ?? name, load_bearing: true });
|
|
114
|
+
if (url)
|
|
115
|
+
externalContracts.add(url);
|
|
116
|
+
}
|
|
117
|
+
// RestTemplate / WebClient → http.
|
|
118
|
+
if (/\bRestTemplate\b|\bWebClient\b/.test(src)) {
|
|
119
|
+
for (const m of src.matchAll(/["'](https?:\/\/[^"']+)["']/g)) {
|
|
120
|
+
downstream.push({ target: m[1], kind: "http", detail: m[1], load_bearing: false });
|
|
121
|
+
externalContracts.add(m[1]);
|
|
122
|
+
}
|
|
123
|
+
if (["']/g)].length) {
|
|
124
|
+
downstream.push({ target: "rest-client", kind: "http", detail: "RestTemplate/WebClient", load_bearing: false });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// @JmsListener / JmsTemplate → jms (+ destination literal where present).
|
|
128
|
+
for (const m of src.matchAll(/@JmsListener\s*\(([^)]*)\)/g)) {
|
|
129
|
+
const dest = (m[1] ?? "").match(/destination\s*=\s*"([^"]+)"/)?.[1] ?? "jms-destination";
|
|
130
|
+
downstream.push({ target: dest, kind: "jms", detail: dest, load_bearing: true });
|
|
131
|
+
sharedInfra.add(dest);
|
|
132
|
+
}
|
|
133
|
+
if (/\bJmsTemplate\b/.test(src)) {
|
|
134
|
+
downstream.push({ target: "jms", kind: "jms", detail: "JmsTemplate", load_bearing: true });
|
|
135
|
+
}
|
|
136
|
+
// @KafkaListener / KafkaTemplate → kafka.
|
|
137
|
+
for (const m of src.matchAll(/@KafkaListener\s*\(([^)]*)\)/g)) {
|
|
138
|
+
const topic = (m[1] ?? "").match(/topics\s*=\s*\{?\s*"([^"]+)"/)?.[1] ?? "kafka-topic";
|
|
139
|
+
downstream.push({ target: topic, kind: "kafka", detail: topic, load_bearing: true });
|
|
140
|
+
sharedInfra.add(topic);
|
|
141
|
+
}
|
|
142
|
+
if (/\bKafkaTemplate\b/.test(src)) {
|
|
143
|
+
downstream.push({ target: "kafka", kind: "kafka", detail: "KafkaTemplate producer", load_bearing: true });
|
|
144
|
+
}
|
|
145
|
+
// Spring Data repositories + entity/document annotations → datastore kind.
|
|
146
|
+
if (/interface\s+\w+\s+extends\s+\w*Repository\b/.test(src)) {
|
|
147
|
+
if (/@Document\b/.test(src)) {
|
|
148
|
+
downstream.push({ target: "mongo", kind: "mongo", detail: "Spring Data MongoRepository", load_bearing: true });
|
|
149
|
+
}
|
|
150
|
+
else if (/@Entity\b/.test(src)) {
|
|
151
|
+
downstream.push({ target: "jdbc", kind: "jdbc", detail: "Spring Data JpaRepository", load_bearing: true });
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
downstream.push({ target: "datastore", kind: "jdbc", detail: "Spring Data Repository", load_bearing: true });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else if (/@Document\b/.test(src)) {
|
|
158
|
+
downstream.push({ target: "mongo", kind: "mongo", detail: "@Document entity", load_bearing: true });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function collectConfigInfra(fileSet, sharedInfra) {
|
|
162
|
+
const configs = fileSet.files.filter((f) => /application(-[\w]+)?\.(ya?ml|properties)$/.test(f));
|
|
163
|
+
for (const path of configs) {
|
|
164
|
+
const src = await fileSet.read(path);
|
|
165
|
+
if (src === "")
|
|
166
|
+
continue;
|
|
167
|
+
// datasource url, kafka topics, jms queues, mongo uri.
|
|
168
|
+
for (const m of src.matchAll(/(?:datasource[._]url|spring\.datasource\.url)\s*[:=]\s*(\S+)/gi)) {
|
|
169
|
+
sharedInfra.add(`jdbc:${stripVar(m[1])}`);
|
|
170
|
+
}
|
|
171
|
+
for (const m of src.matchAll(/(?:topic|telemetrytopic)\s*[:=]\s*(\S+)/gi)) {
|
|
172
|
+
const v = stripVar(m[1]);
|
|
173
|
+
if (v)
|
|
174
|
+
sharedInfra.add(`topic:${v}`);
|
|
175
|
+
}
|
|
176
|
+
for (const m of src.matchAll(/(?:mongo[._]?uri|spring\.data\.mongodb\.uri)\s*[:=]\s*(\S+)/gi)) {
|
|
177
|
+
sharedInfra.add(`mongo:${stripVar(m[1])}`);
|
|
178
|
+
}
|
|
179
|
+
for (const m of src.matchAll(/bootstrap-servers\s*[:=]\s*(\S+)/gi)) {
|
|
180
|
+
sharedInfra.add(`kafka:${stripVar(m[1])}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function stripVar(v) {
|
|
185
|
+
// Keep env-var placeholders readable: `${GATEWAY_KAFKAPRODUCERTOPIC}` → GATEWAY_KAFKAPRODUCERTOPIC.
|
|
186
|
+
const m = v.match(/\$\{([^:}]+)/);
|
|
187
|
+
if (m)
|
|
188
|
+
return m[1];
|
|
189
|
+
return v.replace(/^['"]|['"]$/g, "").trim();
|
|
190
|
+
}
|
|
191
|
+
async function detectJavaRuntime(fileSet) {
|
|
192
|
+
const pom = findBySuffix(fileSet.files, "pom.xml");
|
|
193
|
+
if (pom) {
|
|
194
|
+
const src = await fileSet.read(pom);
|
|
195
|
+
const v = src.match(/<maven\.compiler\.release>\s*([\d.]+)/)?.[1] ??
|
|
196
|
+
src.match(/<maven\.compiler\.target>\s*([\d.]+)/)?.[1] ??
|
|
197
|
+
src.match(/<maven\.compiler\.source>\s*([\d.]+)/)?.[1] ??
|
|
198
|
+
src.match(/<java\.version>\s*([\d.]+)/)?.[1];
|
|
199
|
+
if (v)
|
|
200
|
+
return `java${v}`;
|
|
201
|
+
}
|
|
202
|
+
const gradle = findBySuffix(fileSet.files, "build.gradle");
|
|
203
|
+
if (gradle) {
|
|
204
|
+
const src = await fileSet.read(gradle);
|
|
205
|
+
const v = src.match(/JavaLanguageVersion\.of\(\s*([\d]+)\s*\)/)?.[1] ??
|
|
206
|
+
src.match(/(?:source|target)Compatibility\s*=?\s*['"]?(?:JavaVersion\.VERSION_)?([\d.]+)/)?.[1] ??
|
|
207
|
+
src.match(/java-openjdk(\d+)/)?.[1];
|
|
208
|
+
if (v)
|
|
209
|
+
return `java${v}`;
|
|
210
|
+
}
|
|
211
|
+
return "";
|
|
212
|
+
}
|
|
213
|
+
function joinPaths(base, sub) {
|
|
214
|
+
const b = base.replace(/\/$/, "");
|
|
215
|
+
const s = sub.startsWith("/") ? sub : sub ? `/${sub}` : "";
|
|
216
|
+
const joined = `${b}${s}`;
|
|
217
|
+
return joined.startsWith("/") ? joined : `/${joined}`;
|
|
218
|
+
}
|
|
219
|
+
function lookBack(lines, idx, n) {
|
|
220
|
+
return lines.slice(Math.max(0, idx - n), idx + 1).join("\n");
|
|
221
|
+
}
|
|
222
|
+
function baseName(p) {
|
|
223
|
+
return p.split("/").pop() ?? p;
|
|
224
|
+
}
|
|
225
|
+
function dedupeDownstream(deps) {
|
|
226
|
+
const seen = new Set();
|
|
227
|
+
for (let i = deps.length - 1; i >= 0; i--) {
|
|
228
|
+
const dep = deps[i];
|
|
229
|
+
const key = `${dep.kind}|${dep.detail}`;
|
|
230
|
+
if (seen.has(key))
|
|
231
|
+
deps.splice(i, 1);
|
|
232
|
+
else
|
|
233
|
+
seen.add(key);
|
|
234
|
+
}
|
|
235
|
+
deps.sort((a, b) => (a.kind + a.detail).localeCompare(b.kind + b.detail));
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=spring.js.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { allBySuffix } from "../file-set.js";
|
|
2
|
+
// terraform analyzer. No endpoints. Enumerate `resource "<type>" "<name>"` (grouped by type with
|
|
3
|
+
// counts), `data "<type>"`, `module "<name>"`, and IAM roles. Provisioned resources → shared_infra
|
|
4
|
+
// + a provisioned_resources summary; IAM roles + module sources → deployment.notes.
|
|
5
|
+
const RESOURCE_RE = /resource\s+"([^"]+)"\s+"([^"]+)"/g;
|
|
6
|
+
const DATA_RE = /data\s+"([^"]+)"\s+"([^"]+)"/g;
|
|
7
|
+
const MODULE_RE = /module\s+"([^"]+)"\s*\{/g;
|
|
8
|
+
const MODULE_SOURCE_RE = /module\s+"([^"]+)"\s*\{[^}]*?source\s*=\s*"([^"]+)"/gs;
|
|
9
|
+
const INFRA_RESOURCE_TYPES = new Set([
|
|
10
|
+
"aws_sqs_queue",
|
|
11
|
+
"aws_sns_topic",
|
|
12
|
+
"aws_dynamodb_table",
|
|
13
|
+
"aws_s3_bucket",
|
|
14
|
+
"aws_kinesis_stream",
|
|
15
|
+
"aws_lambda_function",
|
|
16
|
+
"aws_db_instance",
|
|
17
|
+
"aws_rds_cluster",
|
|
18
|
+
"aws_vpc",
|
|
19
|
+
"aws_cognito_user_pool",
|
|
20
|
+
"aws_cognito_identity_pool",
|
|
21
|
+
"aws_api_gateway_rest_api",
|
|
22
|
+
"aws_apigatewayv2_api",
|
|
23
|
+
]);
|
|
24
|
+
export async function analyzeTerraform(fileSet) {
|
|
25
|
+
const tfFiles = allBySuffix(fileSet.files, ".tf").filter((f) => !/(^|\/)\.terraform\//.test(f));
|
|
26
|
+
const resourceCounts = new Map();
|
|
27
|
+
const sharedInfra = new Set();
|
|
28
|
+
const downstream = [];
|
|
29
|
+
const dataSources = new Set();
|
|
30
|
+
const modules = new Set();
|
|
31
|
+
const iamRoles = [];
|
|
32
|
+
for (const path of tfFiles) {
|
|
33
|
+
const src = await fileSet.read(path);
|
|
34
|
+
if (src === "")
|
|
35
|
+
continue;
|
|
36
|
+
for (const m of src.matchAll(RESOURCE_RE)) {
|
|
37
|
+
const type = m[1];
|
|
38
|
+
const name = m[2];
|
|
39
|
+
resourceCounts.set(type, (resourceCounts.get(type) ?? 0) + 1);
|
|
40
|
+
if (type === "aws_iam_role" || /iam_(role|policy)/.test(type)) {
|
|
41
|
+
iamRoles.push(name);
|
|
42
|
+
}
|
|
43
|
+
if (INFRA_RESOURCE_TYPES.has(type)) {
|
|
44
|
+
sharedInfra.add(`${type}.${name}`);
|
|
45
|
+
}
|
|
46
|
+
pushDependency(type, name, downstream);
|
|
47
|
+
}
|
|
48
|
+
for (const m of src.matchAll(DATA_RE)) {
|
|
49
|
+
dataSources.add(`${m[1]}.${m[2]}`);
|
|
50
|
+
}
|
|
51
|
+
for (const m of src.matchAll(MODULE_RE)) {
|
|
52
|
+
modules.add(m[1]);
|
|
53
|
+
}
|
|
54
|
+
for (const m of src.matchAll(MODULE_SOURCE_RE)) {
|
|
55
|
+
modules.add(`${m[1]} (source=${m[2]})`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const provisioned = [...resourceCounts.entries()]
|
|
59
|
+
.map(([type, count]) => ({ type, count }))
|
|
60
|
+
.sort((a, b) => b.count - a.count || a.type.localeCompare(b.type));
|
|
61
|
+
const notes = [];
|
|
62
|
+
if (iamRoles.length > 0) {
|
|
63
|
+
notes.push(`iam roles/policies: ${[...new Set(iamRoles)].sort().join(", ")}`);
|
|
64
|
+
}
|
|
65
|
+
if (dataSources.size > 0) {
|
|
66
|
+
notes.push(`data sources: ${[...dataSources].sort().join(", ")}`);
|
|
67
|
+
}
|
|
68
|
+
if (modules.size > 0) {
|
|
69
|
+
notes.push(`modules: ${[...modules].sort().join(", ")}`);
|
|
70
|
+
}
|
|
71
|
+
dedupeDownstream(downstream);
|
|
72
|
+
return {
|
|
73
|
+
stack: "terraform",
|
|
74
|
+
component_type: "infra",
|
|
75
|
+
endpoints: [],
|
|
76
|
+
dependencies: { downstream, upstream: [] },
|
|
77
|
+
external_contracts: [],
|
|
78
|
+
deployment: {
|
|
79
|
+
runtime: "terraform",
|
|
80
|
+
shared_infra: [...sharedInfra].sort(),
|
|
81
|
+
provisioned_resources: provisioned,
|
|
82
|
+
...(notes.length > 0 ? { notes } : {}),
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function pushDependency(type, name, downstream) {
|
|
87
|
+
const target = `${type}.${name}`;
|
|
88
|
+
if (type === "aws_sqs_queue")
|
|
89
|
+
downstream.push({ target, kind: "sqs", detail: name, load_bearing: true });
|
|
90
|
+
else if (type === "aws_sns_topic")
|
|
91
|
+
downstream.push({ target, kind: "sns", detail: name, load_bearing: true });
|
|
92
|
+
else if (type === "aws_dynamodb_table")
|
|
93
|
+
downstream.push({ target, kind: "dynamodb", detail: name, load_bearing: true });
|
|
94
|
+
else if (type === "aws_s3_bucket")
|
|
95
|
+
downstream.push({ target, kind: "s3", detail: name, load_bearing: false });
|
|
96
|
+
}
|
|
97
|
+
function dedupeDownstream(deps) {
|
|
98
|
+
const seen = new Set();
|
|
99
|
+
for (let i = deps.length - 1; i >= 0; i--) {
|
|
100
|
+
const dep = deps[i];
|
|
101
|
+
const key = `${dep.kind}|${dep.target}`;
|
|
102
|
+
if (seen.has(key))
|
|
103
|
+
deps.splice(i, 1);
|
|
104
|
+
else
|
|
105
|
+
seen.add(key);
|
|
106
|
+
}
|
|
107
|
+
deps.sort((a, b) => (a.kind + a.target).localeCompare(b.kind + b.target));
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=terraform.js.map
|