@suss/manifest-aws 0.0.2 → 0.1.1
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/dist/index.cjs +429 -35
- package/dist/index.d.cts +105 -16
- package/dist/index.d.ts +105 -16
- package/dist/index.js +421 -34
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -31,10 +31,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
CLOUDFORMATION_YAML_TAGS: () => CLOUDFORMATION_YAML_TAGS,
|
|
34
|
+
MAX_STACK_DEPTH: () => MAX_STACK_DEPTH,
|
|
35
|
+
inheritedEnvVars: () => inheritedEnvVars,
|
|
34
36
|
loadCloudFormationTemplate: () => loadCloudFormationTemplate,
|
|
37
|
+
loadTemplateTree: () => loadTemplateTree,
|
|
35
38
|
parseHandler: () => parseHandler,
|
|
39
|
+
qualifiedLogicalId: () => qualifiedLogicalId,
|
|
40
|
+
readAppSyncResolvers: () => readAppSyncResolvers,
|
|
36
41
|
readServerlessFunctions: () => readServerlessFunctions,
|
|
37
|
-
refTarget: () => refTarget
|
|
42
|
+
refTarget: () => refTarget,
|
|
43
|
+
resourcesWithGlobals: () => resourcesWithGlobals,
|
|
44
|
+
unfollowedStackMessage: () => unfollowedStackMessage
|
|
38
45
|
});
|
|
39
46
|
module.exports = __toCommonJS(index_exports);
|
|
40
47
|
|
|
@@ -42,17 +49,27 @@ module.exports = __toCommonJS(index_exports);
|
|
|
42
49
|
var import_node_fs = __toESM(require("fs"), 1);
|
|
43
50
|
var import_node_path = __toESM(require("path"), 1);
|
|
44
51
|
var import_yaml = __toESM(require("yaml"), 1);
|
|
52
|
+
var everyNodeKind = (tag, resolve) => {
|
|
53
|
+
const forNode = (value) => resolve(plainly(value));
|
|
54
|
+
return [
|
|
55
|
+
{ tag, resolve: forNode },
|
|
56
|
+
{ tag, collection: "seq", resolve: forNode },
|
|
57
|
+
{ tag, collection: "map", resolve: forNode }
|
|
58
|
+
];
|
|
59
|
+
};
|
|
60
|
+
var plainly = (value) => typeof value === "object" && value !== null && "toJSON" in value && typeof value.toJSON === "function" ? value.toJSON() : value;
|
|
61
|
+
var getAttParts = (value) => {
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
return value.map(String);
|
|
64
|
+
}
|
|
65
|
+
const text = String(value);
|
|
66
|
+
return text.includes(".") ? text.split(".") : [text];
|
|
67
|
+
};
|
|
45
68
|
var CLOUDFORMATION_YAML_TAGS = [
|
|
46
|
-
{
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
tag: "!GetAtt",
|
|
52
|
-
resolve: (value) => ({
|
|
53
|
-
"Fn::GetAtt": value.includes(".") ? value.split(".") : [value]
|
|
54
|
-
})
|
|
55
|
-
},
|
|
69
|
+
...everyNodeKind("!Ref", (value) => ({ Ref: value })),
|
|
70
|
+
...everyNodeKind("!GetAtt", (value) => ({
|
|
71
|
+
"Fn::GetAtt": getAttParts(value)
|
|
72
|
+
})),
|
|
56
73
|
...[
|
|
57
74
|
"!Sub",
|
|
58
75
|
"!Join",
|
|
@@ -67,7 +84,7 @@ var CLOUDFORMATION_YAML_TAGS = [
|
|
|
67
84
|
"!And",
|
|
68
85
|
"!Or",
|
|
69
86
|
"!Equals"
|
|
70
|
-
].
|
|
87
|
+
].flatMap((tag) => everyNodeKind(tag, (value) => value))
|
|
71
88
|
];
|
|
72
89
|
function loadCloudFormationTemplate(templatePath) {
|
|
73
90
|
const resolved = import_node_path.default.resolve(templatePath);
|
|
@@ -97,10 +114,393 @@ function refTarget(value) {
|
|
|
97
114
|
if (Array.isArray(getAtt) && typeof getAtt[0] === "string") {
|
|
98
115
|
return getAtt[0];
|
|
99
116
|
}
|
|
117
|
+
if (typeof getAtt === "string") {
|
|
118
|
+
const dot = getAtt.indexOf(".");
|
|
119
|
+
return dot === -1 ? getAtt : getAtt.slice(0, dot);
|
|
120
|
+
}
|
|
100
121
|
return null;
|
|
101
122
|
}
|
|
102
123
|
|
|
124
|
+
// src/appsyncResolvers.ts
|
|
125
|
+
var LAMBDA_CATEGORIES = ["Lambda", "Lambdas"];
|
|
126
|
+
function readAppSyncResolvers(template) {
|
|
127
|
+
const resources = template.Resources ?? {};
|
|
128
|
+
return [
|
|
129
|
+
...fromServerlessShorthand(resources),
|
|
130
|
+
...fromRawResources(resources)
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
function fromServerlessShorthand(resources) {
|
|
134
|
+
const bindings = [];
|
|
135
|
+
for (const resource of Object.values(resources)) {
|
|
136
|
+
if (resource?.Type !== "AWS::Serverless::GraphQLApi") {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const props = asRecord(resource.Properties) ?? {};
|
|
140
|
+
const lambdaByDataSource = shorthandDataSources(props.DataSources);
|
|
141
|
+
const dataSourceByFunction = shorthandFunctions(props.Functions);
|
|
142
|
+
for (const [typeName, fields] of entriesOf(props.Resolvers)) {
|
|
143
|
+
for (const [fieldName, config] of entriesOf(fields)) {
|
|
144
|
+
const dataSources = dataSourcesReached(config, dataSourceByFunction);
|
|
145
|
+
bindings.push({
|
|
146
|
+
typeName,
|
|
147
|
+
fieldName,
|
|
148
|
+
lambdaFunctionLogicalIds: dataSources.map((name) => lambdaByDataSource.get(name) ?? null).filter((id) => id !== null)
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return bindings;
|
|
154
|
+
}
|
|
155
|
+
function shorthandDataSources(block) {
|
|
156
|
+
const out = /* @__PURE__ */ new Map();
|
|
157
|
+
for (const [category, entries] of entriesOf(block)) {
|
|
158
|
+
if (!LAMBDA_CATEGORIES.includes(category)) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
for (const [name, config] of entriesOf(entries)) {
|
|
162
|
+
const props = asRecord(config);
|
|
163
|
+
out.set(name, props === null ? null : refTarget(props.FunctionArn));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
function shorthandFunctions(block) {
|
|
169
|
+
const out = /* @__PURE__ */ new Map();
|
|
170
|
+
for (const [name, config] of entriesOf(block)) {
|
|
171
|
+
const props = asRecord(config);
|
|
172
|
+
const dataSource = props === null ? null : stringOf(props.DataSource);
|
|
173
|
+
if (dataSource !== null) {
|
|
174
|
+
out.set(name, dataSource);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
function dataSourcesReached(config, dataSourceByFunction) {
|
|
180
|
+
const props = asRecord(config);
|
|
181
|
+
if (props === null) {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
const direct = stringOf(props.DataSource);
|
|
185
|
+
if (direct !== null) {
|
|
186
|
+
return [direct];
|
|
187
|
+
}
|
|
188
|
+
const pipeline = Array.isArray(props.Pipeline) ? props.Pipeline : [];
|
|
189
|
+
return pipeline.map((name) => dataSourceByFunction.get(String(name))).filter((name) => name !== void 0);
|
|
190
|
+
}
|
|
191
|
+
function fromRawResources(resources) {
|
|
192
|
+
const lambdaByLogicalId = /* @__PURE__ */ new Map();
|
|
193
|
+
const lambdaByApiAndName = /* @__PURE__ */ new Map();
|
|
194
|
+
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
195
|
+
if (resource?.Type !== "AWS::AppSync::DataSource") {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const props = asRecord(resource.Properties) ?? {};
|
|
199
|
+
const lambdaConfig = asRecord(props.LambdaConfig);
|
|
200
|
+
const lambda = lambdaConfig === null ? null : refTarget(lambdaConfig.LambdaFunctionArn);
|
|
201
|
+
if (lambda === null) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
lambdaByLogicalId.set(logicalId, lambda);
|
|
205
|
+
const name = stringOf(props.Name);
|
|
206
|
+
if (name !== null) {
|
|
207
|
+
lambdaByApiAndName.set(
|
|
208
|
+
apiScopedKey(refTarget(props.ApiId), name),
|
|
209
|
+
lambda
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const dataSourceByLogicalId = /* @__PURE__ */ new Map();
|
|
214
|
+
const dataSourceByApiAndName = /* @__PURE__ */ new Map();
|
|
215
|
+
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
216
|
+
if (resource?.Type !== "AWS::AppSync::FunctionConfiguration") {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const props = asRecord(resource.Properties) ?? {};
|
|
220
|
+
const dataSource = refTarget(props.DataSourceName);
|
|
221
|
+
if (dataSource === null) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
dataSourceByLogicalId.set(logicalId, dataSource);
|
|
225
|
+
const name = stringOf(props.Name);
|
|
226
|
+
if (name !== null) {
|
|
227
|
+
dataSourceByApiAndName.set(
|
|
228
|
+
apiScopedKey(refTarget(props.ApiId), name),
|
|
229
|
+
dataSource
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const bindings = [];
|
|
234
|
+
for (const resource of Object.values(resources)) {
|
|
235
|
+
if (resource?.Type !== "AWS::AppSync::Resolver") {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const props = asRecord(resource.Properties) ?? {};
|
|
239
|
+
const typeName = stringOf(props.TypeName);
|
|
240
|
+
const fieldName = stringOf(props.FieldName);
|
|
241
|
+
if (typeName === null || fieldName === null) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const apiId = refTarget(props.ApiId);
|
|
245
|
+
const lookup = (byLogicalId, byName) => (reference) => byLogicalId.get(reference) ?? byName.get(apiScopedKey(apiId, reference));
|
|
246
|
+
bindings.push({
|
|
247
|
+
typeName,
|
|
248
|
+
fieldName,
|
|
249
|
+
lambdaFunctionLogicalIds: rawDataSourcesReached(
|
|
250
|
+
props,
|
|
251
|
+
lookup(dataSourceByLogicalId, dataSourceByApiAndName)
|
|
252
|
+
).map(lookup(lambdaByLogicalId, lambdaByApiAndName)).filter((id) => id !== void 0)
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return bindings;
|
|
256
|
+
}
|
|
257
|
+
function apiScopedKey(apiId, name) {
|
|
258
|
+
return `${apiId ?? ""}::${name}`;
|
|
259
|
+
}
|
|
260
|
+
function rawDataSourcesReached(props, dataSourceOf) {
|
|
261
|
+
const direct = refTarget(props.DataSourceName);
|
|
262
|
+
if (direct !== null) {
|
|
263
|
+
return [direct];
|
|
264
|
+
}
|
|
265
|
+
const pipeline = asRecord(props.PipelineConfig);
|
|
266
|
+
const functions = Array.isArray(pipeline?.Functions) ? pipeline.Functions : [];
|
|
267
|
+
return functions.map((fn) => {
|
|
268
|
+
const reference = refTarget(fn);
|
|
269
|
+
return reference === null ? void 0 : dataSourceOf(reference);
|
|
270
|
+
}).filter((name) => name !== void 0);
|
|
271
|
+
}
|
|
272
|
+
function asRecord(value) {
|
|
273
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
274
|
+
}
|
|
275
|
+
function entriesOf(value) {
|
|
276
|
+
const record = asRecord(value);
|
|
277
|
+
return record === null ? [] : Object.entries(record);
|
|
278
|
+
}
|
|
279
|
+
function stringOf(value) {
|
|
280
|
+
return typeof value === "string" ? value : null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/globals.ts
|
|
284
|
+
var GLOBALS_SECTION_TYPES = {
|
|
285
|
+
Api: "AWS::Serverless::Api",
|
|
286
|
+
Function: "AWS::Serverless::Function",
|
|
287
|
+
HttpApi: "AWS::Serverless::HttpApi",
|
|
288
|
+
LayerVersion: "AWS::Serverless::LayerVersion",
|
|
289
|
+
SimpleTable: "AWS::Serverless::SimpleTable",
|
|
290
|
+
StateMachine: "AWS::Serverless::StateMachine"
|
|
291
|
+
};
|
|
292
|
+
function resourcesWithGlobals(template) {
|
|
293
|
+
const resources = template.Resources ?? {};
|
|
294
|
+
const defaultsByType = defaultsPerResourceType(template.Globals);
|
|
295
|
+
if (defaultsByType.size === 0) {
|
|
296
|
+
return resources;
|
|
297
|
+
}
|
|
298
|
+
const out = {};
|
|
299
|
+
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
300
|
+
const defaults = defaultsByType.get(resource.Type ?? "");
|
|
301
|
+
out[logicalId] = defaults === void 0 ? resource : {
|
|
302
|
+
...resource,
|
|
303
|
+
Properties: mergedProperties(defaults, resource.Properties ?? {})
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
308
|
+
function inheritedEnvVars(template) {
|
|
309
|
+
const sectionVars = envVarNames(template.Globals?.Function);
|
|
310
|
+
if (sectionVars.length === 0) {
|
|
311
|
+
return {};
|
|
312
|
+
}
|
|
313
|
+
const out = {};
|
|
314
|
+
for (const [logicalId, resource] of Object.entries(
|
|
315
|
+
template.Resources ?? {}
|
|
316
|
+
)) {
|
|
317
|
+
if (resource.Type !== GLOBALS_SECTION_TYPES.Function) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const own = new Set(envVarNames(resource.Properties));
|
|
321
|
+
out[logicalId] = sectionVars.filter((name) => !own.has(name));
|
|
322
|
+
}
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
function envVarNames(properties) {
|
|
326
|
+
const environment = properties?.Environment;
|
|
327
|
+
if (!isPlainObject(environment)) {
|
|
328
|
+
return [];
|
|
329
|
+
}
|
|
330
|
+
const variables = environment.Variables;
|
|
331
|
+
return isPlainObject(variables) ? Object.keys(variables) : [];
|
|
332
|
+
}
|
|
333
|
+
function defaultsPerResourceType(globals) {
|
|
334
|
+
const byType = /* @__PURE__ */ new Map();
|
|
335
|
+
if (globals === null || typeof globals !== "object") {
|
|
336
|
+
return byType;
|
|
337
|
+
}
|
|
338
|
+
for (const [section, properties] of Object.entries(globals)) {
|
|
339
|
+
const resourceType = GLOBALS_SECTION_TYPES[section];
|
|
340
|
+
if (resourceType === void 0 || !isPlainObject(properties)) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
byType.set(resourceType, properties);
|
|
344
|
+
}
|
|
345
|
+
return byType;
|
|
346
|
+
}
|
|
347
|
+
function mergedProperties(defaults, own) {
|
|
348
|
+
const merged = { ...own };
|
|
349
|
+
for (const [name, fallback] of Object.entries(defaults)) {
|
|
350
|
+
merged[name] = name in own ? combined(fallback, own[name]) : fallback;
|
|
351
|
+
}
|
|
352
|
+
return merged;
|
|
353
|
+
}
|
|
354
|
+
function combined(fallback, own) {
|
|
355
|
+
if (Array.isArray(fallback) && Array.isArray(own)) {
|
|
356
|
+
return [...fallback, ...own];
|
|
357
|
+
}
|
|
358
|
+
if (isMergeableMap(fallback) && isMergeableMap(own)) {
|
|
359
|
+
return mergedProperties(fallback, own);
|
|
360
|
+
}
|
|
361
|
+
return own;
|
|
362
|
+
}
|
|
363
|
+
function isMergeableMap(value) {
|
|
364
|
+
return isPlainObject(value) && !isIntrinsic(value);
|
|
365
|
+
}
|
|
366
|
+
function isPlainObject(value) {
|
|
367
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
368
|
+
}
|
|
369
|
+
function isIntrinsic(value) {
|
|
370
|
+
const keys = Object.keys(value);
|
|
371
|
+
return keys.length === 1 && (keys[0] === "Ref" || keys[0].startsWith("Fn::") || keys[0] === "Condition");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/nestedStacks.ts
|
|
375
|
+
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
376
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
377
|
+
var CHILD_TEMPLATE_PROPERTY = {
|
|
378
|
+
"AWS::CloudFormation::Stack": "TemplateURL",
|
|
379
|
+
"AWS::Serverless::Application": "Location"
|
|
380
|
+
};
|
|
381
|
+
var MAX_STACK_DEPTH = 10;
|
|
382
|
+
function loadTemplateTree(rootPath) {
|
|
383
|
+
const rootFile = import_node_path2.default.resolve(rootPath);
|
|
384
|
+
const root = {
|
|
385
|
+
path: rootFile,
|
|
386
|
+
stackPath: [],
|
|
387
|
+
template: loadCloudFormationTemplate(rootFile)
|
|
388
|
+
};
|
|
389
|
+
const documents = [root];
|
|
390
|
+
const unfollowed = [];
|
|
391
|
+
const queue = [
|
|
392
|
+
{ document: root, ancestors: [rootFile] }
|
|
393
|
+
];
|
|
394
|
+
for (let i = 0; i < queue.length; i += 1) {
|
|
395
|
+
const { document, ancestors } = queue[i];
|
|
396
|
+
for (const child of childStacksOf(document, ancestors)) {
|
|
397
|
+
if (child.type === "unfollowed") {
|
|
398
|
+
unfollowed.push(child.stack);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
documents.push(child.document);
|
|
402
|
+
queue.push({
|
|
403
|
+
document: child.document,
|
|
404
|
+
ancestors: [...ancestors, child.document.path]
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return { documents, unfollowed };
|
|
409
|
+
}
|
|
410
|
+
function qualifiedLogicalId(stackPath, logicalId) {
|
|
411
|
+
return [...stackPath, logicalId].join("/");
|
|
412
|
+
}
|
|
413
|
+
function unfollowedStackMessage(stack) {
|
|
414
|
+
const name = stack.stackPath.join("/");
|
|
415
|
+
return `could not follow nested stack ${name} declared in ${stack.declaredIn}: ${stack.detail}`;
|
|
416
|
+
}
|
|
417
|
+
function childStacksOf(document, ancestors) {
|
|
418
|
+
const out = [];
|
|
419
|
+
for (const [logicalId, resource] of Object.entries(
|
|
420
|
+
document.template.Resources ?? {}
|
|
421
|
+
)) {
|
|
422
|
+
const property = CHILD_TEMPLATE_PROPERTY[resource.Type ?? ""];
|
|
423
|
+
if (property === void 0) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
out.push(
|
|
427
|
+
childStack({
|
|
428
|
+
document,
|
|
429
|
+
ancestors,
|
|
430
|
+
logicalId,
|
|
431
|
+
location: resource.Properties?.[property]
|
|
432
|
+
})
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
return out;
|
|
436
|
+
}
|
|
437
|
+
function childStack(opts) {
|
|
438
|
+
const stackPath = [...opts.document.stackPath, opts.logicalId];
|
|
439
|
+
const report = (reason, detail, templateUrl) => ({
|
|
440
|
+
type: "unfollowed",
|
|
441
|
+
stack: {
|
|
442
|
+
declaredIn: opts.document.path,
|
|
443
|
+
stackPath,
|
|
444
|
+
templateUrl,
|
|
445
|
+
reason,
|
|
446
|
+
detail
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
const url = typeof opts.location === "string" ? opts.location.trim() : null;
|
|
450
|
+
if (url === null || url === "") {
|
|
451
|
+
return report(
|
|
452
|
+
"notALiteralPath",
|
|
453
|
+
"the template location is not a literal string",
|
|
454
|
+
null
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
if (url.includes("${")) {
|
|
458
|
+
return report(
|
|
459
|
+
"notALiteralPath",
|
|
460
|
+
`${url} holds a substitution nothing here can fill in`,
|
|
461
|
+
url
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
if (hasUrlScheme(url)) {
|
|
465
|
+
return report("remoteUrl", `${url} is not a path in this repository`, url);
|
|
466
|
+
}
|
|
467
|
+
if (stackPath.length > MAX_STACK_DEPTH) {
|
|
468
|
+
return report(
|
|
469
|
+
"depthLimit",
|
|
470
|
+
`nesting is deeper than ${MAX_STACK_DEPTH} stacks`,
|
|
471
|
+
url
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
const childPath = import_node_path2.default.resolve(import_node_path2.default.dirname(opts.document.path), url);
|
|
475
|
+
if (opts.ancestors.includes(childPath)) {
|
|
476
|
+
return report("cycle", `${url} is already open further up the tree`, url);
|
|
477
|
+
}
|
|
478
|
+
if (!import_node_fs2.default.existsSync(childPath)) {
|
|
479
|
+
return report("fileMissing", `${url} is not on disk`, url);
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
return {
|
|
483
|
+
type: "document",
|
|
484
|
+
document: {
|
|
485
|
+
path: childPath,
|
|
486
|
+
stackPath,
|
|
487
|
+
template: loadCloudFormationTemplate(childPath)
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
} catch (err) {
|
|
491
|
+
return report(
|
|
492
|
+
"unreadable",
|
|
493
|
+
`${url} could not be parsed: ${err instanceof Error ? err.message : String(err)}`,
|
|
494
|
+
url
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function hasUrlScheme(location) {
|
|
499
|
+
return /^[a-z][a-z0-9+.-]*:\/\//i.test(location);
|
|
500
|
+
}
|
|
501
|
+
|
|
103
502
|
// src/serverlessFunctions.ts
|
|
503
|
+
var import_ir_core = require("@suss/ir-core");
|
|
104
504
|
function parseHandler(handler) {
|
|
105
505
|
const trimmed = handler.trim();
|
|
106
506
|
const lastDot = trimmed.lastIndexOf(".");
|
|
@@ -112,24 +512,12 @@ function parseHandler(handler) {
|
|
|
112
512
|
exportName: trimmed.slice(lastDot + 1)
|
|
113
513
|
};
|
|
114
514
|
}
|
|
115
|
-
function
|
|
116
|
-
const trimmed = raw.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
117
|
-
return trimmed === "" ? "." : trimmed;
|
|
118
|
-
}
|
|
119
|
-
function readGlobalsCodeUri(template) {
|
|
120
|
-
const fnGlobals = template.Globals?.Function;
|
|
121
|
-
if (fnGlobals === void 0) {
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
const codeUri = fnGlobals.CodeUri;
|
|
125
|
-
return typeof codeUri === "string" ? normalizeCodeUri(codeUri) : null;
|
|
126
|
-
}
|
|
127
|
-
function readCodeUri(resource, globalsCodeUri) {
|
|
515
|
+
function readCodeUri(resource) {
|
|
128
516
|
const codeUri = resource.Properties?.CodeUri;
|
|
129
|
-
if (typeof codeUri
|
|
130
|
-
return
|
|
517
|
+
if (typeof codeUri !== "string") {
|
|
518
|
+
return ".";
|
|
131
519
|
}
|
|
132
|
-
return
|
|
520
|
+
return (0, import_ir_core.codeScopePath)(codeUri) || ".";
|
|
133
521
|
}
|
|
134
522
|
function classifyEvents(events) {
|
|
135
523
|
const httpRoutes = [];
|
|
@@ -149,8 +537,8 @@ function classifyEvents(events) {
|
|
|
149
537
|
}
|
|
150
538
|
const props = event.Properties ?? {};
|
|
151
539
|
const method = String(props.Method ?? "").toUpperCase();
|
|
152
|
-
const
|
|
153
|
-
if (method === "" ||
|
|
540
|
+
const path3 = String(props.Path ?? "");
|
|
541
|
+
if (method === "" || path3 === "") {
|
|
154
542
|
nonHttpEvents.push({ eventId, eventType: type });
|
|
155
543
|
continue;
|
|
156
544
|
}
|
|
@@ -159,15 +547,14 @@ function classifyEvents(events) {
|
|
|
159
547
|
eventId,
|
|
160
548
|
eventType: type,
|
|
161
549
|
method,
|
|
162
|
-
path:
|
|
550
|
+
path: path3,
|
|
163
551
|
apiId: apiRef
|
|
164
552
|
});
|
|
165
553
|
}
|
|
166
554
|
return { httpRoutes, nonHttpEvents };
|
|
167
555
|
}
|
|
168
556
|
function readServerlessFunctions(template) {
|
|
169
|
-
const resources = template
|
|
170
|
-
const globalsCodeUri = readGlobalsCodeUri(template);
|
|
557
|
+
const resources = resourcesWithGlobals(template);
|
|
171
558
|
const out = [];
|
|
172
559
|
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
173
560
|
if (resource.Type !== "AWS::Serverless::Function") {
|
|
@@ -189,7 +576,7 @@ function readServerlessFunctions(template) {
|
|
|
189
576
|
handler: handlerRaw,
|
|
190
577
|
modulePath: parsed.modulePath,
|
|
191
578
|
exportName: parsed.exportName,
|
|
192
|
-
codeUri: readCodeUri(resource
|
|
579
|
+
codeUri: readCodeUri(resource),
|
|
193
580
|
httpRoutes,
|
|
194
581
|
nonHttpEvents
|
|
195
582
|
});
|
|
@@ -199,8 +586,15 @@ function readServerlessFunctions(template) {
|
|
|
199
586
|
// Annotate the CommonJS export names for ESM import in node:
|
|
200
587
|
0 && (module.exports = {
|
|
201
588
|
CLOUDFORMATION_YAML_TAGS,
|
|
589
|
+
MAX_STACK_DEPTH,
|
|
590
|
+
inheritedEnvVars,
|
|
202
591
|
loadCloudFormationTemplate,
|
|
592
|
+
loadTemplateTree,
|
|
203
593
|
parseHandler,
|
|
594
|
+
qualifiedLogicalId,
|
|
595
|
+
readAppSyncResolvers,
|
|
204
596
|
readServerlessFunctions,
|
|
205
|
-
refTarget
|
|
597
|
+
refTarget,
|
|
598
|
+
resourcesWithGlobals,
|
|
599
|
+
unfollowedStackMessage
|
|
206
600
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import YAML from 'yaml';
|
|
2
|
+
|
|
1
3
|
interface CloudFormationResource {
|
|
2
4
|
Type?: string;
|
|
3
5
|
Properties?: Record<string, unknown>;
|
|
@@ -12,20 +14,7 @@ interface CloudFormationTemplate {
|
|
|
12
14
|
Globals?: Record<string, Record<string, unknown>>;
|
|
13
15
|
Resources?: Record<string, CloudFormationResource>;
|
|
14
16
|
}
|
|
15
|
-
declare const CLOUDFORMATION_YAML_TAGS:
|
|
16
|
-
tag: string;
|
|
17
|
-
resolve: (value: unknown) => unknown;
|
|
18
|
-
} | {
|
|
19
|
-
tag: string;
|
|
20
|
-
resolve: (value: string) => {
|
|
21
|
-
Ref: string;
|
|
22
|
-
};
|
|
23
|
-
} | {
|
|
24
|
-
tag: string;
|
|
25
|
-
resolve: (value: string) => {
|
|
26
|
-
"Fn::GetAtt": string[];
|
|
27
|
-
};
|
|
28
|
-
})[];
|
|
17
|
+
declare const CLOUDFORMATION_YAML_TAGS: YAML.CollectionTag[];
|
|
29
18
|
/**
|
|
30
19
|
* Load a CloudFormation / SAM template from disk. Format is detected by
|
|
31
20
|
* extension; `.json` parses as JSON, everything else (including
|
|
@@ -37,13 +26,113 @@ declare const CLOUDFORMATION_YAML_TAGS: ({
|
|
|
37
26
|
*/
|
|
38
27
|
declare function loadCloudFormationTemplate(templatePath: string): CloudFormationTemplate;
|
|
39
28
|
/**
|
|
40
|
-
* CloudFormation references show up in
|
|
29
|
+
* CloudFormation references show up in four shapes after parsing:
|
|
41
30
|
* - { Ref: "LogicalId" }
|
|
42
31
|
* - { "Fn::GetAtt": ["LogicalId", "Attr"] }
|
|
32
|
+
* - { "Fn::GetAtt": "LogicalId.Attr" }, the full-form spelling of the
|
|
33
|
+
* `!GetAtt` short form, which templates written by hand still use
|
|
43
34
|
* - the bare logical id when the parser doesn't recognise the YAML tag
|
|
35
|
+
*
|
|
36
|
+
* Every CloudFormation property that names another resource accepts all
|
|
37
|
+
* of them, so anything reading such a property should come through here
|
|
38
|
+
* rather than matching one shape.
|
|
44
39
|
*/
|
|
45
40
|
declare function refTarget(value: unknown): string | null;
|
|
46
41
|
|
|
42
|
+
/** One GraphQL field and the Lambdas behind it. */
|
|
43
|
+
interface AppSyncResolverBinding {
|
|
44
|
+
typeName: string;
|
|
45
|
+
fieldName: string;
|
|
46
|
+
/**
|
|
47
|
+
* Every Lambda a request for this field runs, in pipeline order.
|
|
48
|
+
* Usually one. A pipeline can chain several, and nothing in the
|
|
49
|
+
* template says which of them is the one that produces the value:
|
|
50
|
+
* the first is as often an auth step as it is the resolver, and the
|
|
51
|
+
* last is as often a formatter. So all of them are reported, and a
|
|
52
|
+
* reader that needs to pick says so itself.
|
|
53
|
+
*
|
|
54
|
+
* Empty when the field is served by something other than a Lambda.
|
|
55
|
+
*/
|
|
56
|
+
lambdaFunctionLogicalIds: string[];
|
|
57
|
+
}
|
|
58
|
+
declare function readAppSyncResolvers(template: CloudFormationTemplate): AppSyncResolverBinding[];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The template's resources with each `Globals` section already applied
|
|
62
|
+
* to the resources it covers. A resource of a type no section names, or
|
|
63
|
+
* a template with no `Globals`, comes back untouched.
|
|
64
|
+
*/
|
|
65
|
+
declare function resourcesWithGlobals(template: CloudFormationTemplate): Record<string, CloudFormationResource>;
|
|
66
|
+
/**
|
|
67
|
+
* Per function logical id, the environment variables the `Function`
|
|
68
|
+
* section supplies that the function does not declare for itself. A
|
|
69
|
+
* consumer that treats a function's environment as its own contract
|
|
70
|
+
* needs the distinction: a name every function in the document receives
|
|
71
|
+
* says something about the document, not about any one function.
|
|
72
|
+
*/
|
|
73
|
+
declare function inheritedEnvVars(template: CloudFormationTemplate): Record<string, string[]>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* How deep the reader follows children. CloudFormation itself allows
|
|
77
|
+
* deeper nesting; a template chain this long in a repository is far
|
|
78
|
+
* more likely to be a mistake than a design, and the limit is reported
|
|
79
|
+
* rather than applied silently.
|
|
80
|
+
*/
|
|
81
|
+
declare const MAX_STACK_DEPTH = 10;
|
|
82
|
+
/** One template file, and where it sits in the tree that reached it. */
|
|
83
|
+
interface TemplateDocument {
|
|
84
|
+
/** Absolute path of the file this document was read from. */
|
|
85
|
+
path: string;
|
|
86
|
+
/**
|
|
87
|
+
* Logical ids of the stack resources leading from the root document
|
|
88
|
+
* down to this one. Empty for the root.
|
|
89
|
+
*/
|
|
90
|
+
stackPath: string[];
|
|
91
|
+
template: CloudFormationTemplate;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Why a child template was not read. Every value is a limit on what the
|
|
95
|
+
* reader could reach, so a consumer should report it as reading that
|
|
96
|
+
* stopped rather than treat the child as a template holding nothing.
|
|
97
|
+
*/
|
|
98
|
+
type UnfollowedReason = "remoteUrl" | "notALiteralPath" | "fileMissing" | "unreadable" | "cycle" | "depthLimit";
|
|
99
|
+
/** A child template the reader could not open, and what stopped it. */
|
|
100
|
+
interface UnfollowedStack {
|
|
101
|
+
/** Absolute path of the document declaring the stack resource. */
|
|
102
|
+
declaredIn: string;
|
|
103
|
+
/** Stack path of the child, ending in this stack resource's logical id. */
|
|
104
|
+
stackPath: string[];
|
|
105
|
+
/** The template location as the document writes it, when it is a string. */
|
|
106
|
+
templateUrl: string | null;
|
|
107
|
+
reason: UnfollowedReason;
|
|
108
|
+
/** One phrase naming what stopped the read, for a message to a person. */
|
|
109
|
+
detail: string;
|
|
110
|
+
}
|
|
111
|
+
interface TemplateTree {
|
|
112
|
+
/** The root document first, then every child that could be read. */
|
|
113
|
+
documents: TemplateDocument[];
|
|
114
|
+
unfollowed: UnfollowedStack[];
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The root template plus every template it embeds, directly or through
|
|
118
|
+
* another child.
|
|
119
|
+
*
|
|
120
|
+
* A missing or malformed ROOT throws, matching
|
|
121
|
+
* `loadCloudFormationTemplate`: the caller asked for that file by name.
|
|
122
|
+
* A child that cannot be read never throws; it lands in `unfollowed` so
|
|
123
|
+
* the caller can report which one and why.
|
|
124
|
+
*/
|
|
125
|
+
declare function loadTemplateTree(rootPath: string): TemplateTree;
|
|
126
|
+
/**
|
|
127
|
+
* A logical id qualified by the stack path that reaches it, so two
|
|
128
|
+
* documents that both declare `HandlerFunction` name two different
|
|
129
|
+
* deployed things. A resource in the root document keeps its bare id,
|
|
130
|
+
* which is also what CloudFormation shows for it.
|
|
131
|
+
*/
|
|
132
|
+
declare function qualifiedLogicalId(stackPath: string[], logicalId: string): string;
|
|
133
|
+
/** A line naming one child the reader could not open, and why. */
|
|
134
|
+
declare function unfollowedStackMessage(stack: UnfollowedStack): string;
|
|
135
|
+
|
|
47
136
|
/** A route-bearing SAM Event (`Type: Api` or `Type: HttpApi`). */
|
|
48
137
|
interface ServerlessHttpRoute {
|
|
49
138
|
/** Key of the event in the function's `Events` map. */
|
|
@@ -104,4 +193,4 @@ declare function parseHandler(handler: string): ParsedHandler | null;
|
|
|
104
193
|
*/
|
|
105
194
|
declare function readServerlessFunctions(template: CloudFormationTemplate): ServerlessFunctionInfo[];
|
|
106
195
|
|
|
107
|
-
export { CLOUDFORMATION_YAML_TAGS, type CloudFormationResource, type CloudFormationTemplate, type ParsedHandler, type ServerlessFunctionInfo, type ServerlessHttpRoute, type ServerlessNonHttpEvent, loadCloudFormationTemplate, parseHandler, readServerlessFunctions, refTarget };
|
|
196
|
+
export { type AppSyncResolverBinding, CLOUDFORMATION_YAML_TAGS, type CloudFormationResource, type CloudFormationTemplate, MAX_STACK_DEPTH, type ParsedHandler, type ServerlessFunctionInfo, type ServerlessHttpRoute, type ServerlessNonHttpEvent, type TemplateDocument, type TemplateTree, type UnfollowedReason, type UnfollowedStack, inheritedEnvVars, loadCloudFormationTemplate, loadTemplateTree, parseHandler, qualifiedLogicalId, readAppSyncResolvers, readServerlessFunctions, refTarget, resourcesWithGlobals, unfollowedStackMessage };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import YAML from 'yaml';
|
|
2
|
+
|
|
1
3
|
interface CloudFormationResource {
|
|
2
4
|
Type?: string;
|
|
3
5
|
Properties?: Record<string, unknown>;
|
|
@@ -12,20 +14,7 @@ interface CloudFormationTemplate {
|
|
|
12
14
|
Globals?: Record<string, Record<string, unknown>>;
|
|
13
15
|
Resources?: Record<string, CloudFormationResource>;
|
|
14
16
|
}
|
|
15
|
-
declare const CLOUDFORMATION_YAML_TAGS:
|
|
16
|
-
tag: string;
|
|
17
|
-
resolve: (value: unknown) => unknown;
|
|
18
|
-
} | {
|
|
19
|
-
tag: string;
|
|
20
|
-
resolve: (value: string) => {
|
|
21
|
-
Ref: string;
|
|
22
|
-
};
|
|
23
|
-
} | {
|
|
24
|
-
tag: string;
|
|
25
|
-
resolve: (value: string) => {
|
|
26
|
-
"Fn::GetAtt": string[];
|
|
27
|
-
};
|
|
28
|
-
})[];
|
|
17
|
+
declare const CLOUDFORMATION_YAML_TAGS: YAML.CollectionTag[];
|
|
29
18
|
/**
|
|
30
19
|
* Load a CloudFormation / SAM template from disk. Format is detected by
|
|
31
20
|
* extension; `.json` parses as JSON, everything else (including
|
|
@@ -37,13 +26,113 @@ declare const CLOUDFORMATION_YAML_TAGS: ({
|
|
|
37
26
|
*/
|
|
38
27
|
declare function loadCloudFormationTemplate(templatePath: string): CloudFormationTemplate;
|
|
39
28
|
/**
|
|
40
|
-
* CloudFormation references show up in
|
|
29
|
+
* CloudFormation references show up in four shapes after parsing:
|
|
41
30
|
* - { Ref: "LogicalId" }
|
|
42
31
|
* - { "Fn::GetAtt": ["LogicalId", "Attr"] }
|
|
32
|
+
* - { "Fn::GetAtt": "LogicalId.Attr" }, the full-form spelling of the
|
|
33
|
+
* `!GetAtt` short form, which templates written by hand still use
|
|
43
34
|
* - the bare logical id when the parser doesn't recognise the YAML tag
|
|
35
|
+
*
|
|
36
|
+
* Every CloudFormation property that names another resource accepts all
|
|
37
|
+
* of them, so anything reading such a property should come through here
|
|
38
|
+
* rather than matching one shape.
|
|
44
39
|
*/
|
|
45
40
|
declare function refTarget(value: unknown): string | null;
|
|
46
41
|
|
|
42
|
+
/** One GraphQL field and the Lambdas behind it. */
|
|
43
|
+
interface AppSyncResolverBinding {
|
|
44
|
+
typeName: string;
|
|
45
|
+
fieldName: string;
|
|
46
|
+
/**
|
|
47
|
+
* Every Lambda a request for this field runs, in pipeline order.
|
|
48
|
+
* Usually one. A pipeline can chain several, and nothing in the
|
|
49
|
+
* template says which of them is the one that produces the value:
|
|
50
|
+
* the first is as often an auth step as it is the resolver, and the
|
|
51
|
+
* last is as often a formatter. So all of them are reported, and a
|
|
52
|
+
* reader that needs to pick says so itself.
|
|
53
|
+
*
|
|
54
|
+
* Empty when the field is served by something other than a Lambda.
|
|
55
|
+
*/
|
|
56
|
+
lambdaFunctionLogicalIds: string[];
|
|
57
|
+
}
|
|
58
|
+
declare function readAppSyncResolvers(template: CloudFormationTemplate): AppSyncResolverBinding[];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The template's resources with each `Globals` section already applied
|
|
62
|
+
* to the resources it covers. A resource of a type no section names, or
|
|
63
|
+
* a template with no `Globals`, comes back untouched.
|
|
64
|
+
*/
|
|
65
|
+
declare function resourcesWithGlobals(template: CloudFormationTemplate): Record<string, CloudFormationResource>;
|
|
66
|
+
/**
|
|
67
|
+
* Per function logical id, the environment variables the `Function`
|
|
68
|
+
* section supplies that the function does not declare for itself. A
|
|
69
|
+
* consumer that treats a function's environment as its own contract
|
|
70
|
+
* needs the distinction: a name every function in the document receives
|
|
71
|
+
* says something about the document, not about any one function.
|
|
72
|
+
*/
|
|
73
|
+
declare function inheritedEnvVars(template: CloudFormationTemplate): Record<string, string[]>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* How deep the reader follows children. CloudFormation itself allows
|
|
77
|
+
* deeper nesting; a template chain this long in a repository is far
|
|
78
|
+
* more likely to be a mistake than a design, and the limit is reported
|
|
79
|
+
* rather than applied silently.
|
|
80
|
+
*/
|
|
81
|
+
declare const MAX_STACK_DEPTH = 10;
|
|
82
|
+
/** One template file, and where it sits in the tree that reached it. */
|
|
83
|
+
interface TemplateDocument {
|
|
84
|
+
/** Absolute path of the file this document was read from. */
|
|
85
|
+
path: string;
|
|
86
|
+
/**
|
|
87
|
+
* Logical ids of the stack resources leading from the root document
|
|
88
|
+
* down to this one. Empty for the root.
|
|
89
|
+
*/
|
|
90
|
+
stackPath: string[];
|
|
91
|
+
template: CloudFormationTemplate;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Why a child template was not read. Every value is a limit on what the
|
|
95
|
+
* reader could reach, so a consumer should report it as reading that
|
|
96
|
+
* stopped rather than treat the child as a template holding nothing.
|
|
97
|
+
*/
|
|
98
|
+
type UnfollowedReason = "remoteUrl" | "notALiteralPath" | "fileMissing" | "unreadable" | "cycle" | "depthLimit";
|
|
99
|
+
/** A child template the reader could not open, and what stopped it. */
|
|
100
|
+
interface UnfollowedStack {
|
|
101
|
+
/** Absolute path of the document declaring the stack resource. */
|
|
102
|
+
declaredIn: string;
|
|
103
|
+
/** Stack path of the child, ending in this stack resource's logical id. */
|
|
104
|
+
stackPath: string[];
|
|
105
|
+
/** The template location as the document writes it, when it is a string. */
|
|
106
|
+
templateUrl: string | null;
|
|
107
|
+
reason: UnfollowedReason;
|
|
108
|
+
/** One phrase naming what stopped the read, for a message to a person. */
|
|
109
|
+
detail: string;
|
|
110
|
+
}
|
|
111
|
+
interface TemplateTree {
|
|
112
|
+
/** The root document first, then every child that could be read. */
|
|
113
|
+
documents: TemplateDocument[];
|
|
114
|
+
unfollowed: UnfollowedStack[];
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The root template plus every template it embeds, directly or through
|
|
118
|
+
* another child.
|
|
119
|
+
*
|
|
120
|
+
* A missing or malformed ROOT throws, matching
|
|
121
|
+
* `loadCloudFormationTemplate`: the caller asked for that file by name.
|
|
122
|
+
* A child that cannot be read never throws; it lands in `unfollowed` so
|
|
123
|
+
* the caller can report which one and why.
|
|
124
|
+
*/
|
|
125
|
+
declare function loadTemplateTree(rootPath: string): TemplateTree;
|
|
126
|
+
/**
|
|
127
|
+
* A logical id qualified by the stack path that reaches it, so two
|
|
128
|
+
* documents that both declare `HandlerFunction` name two different
|
|
129
|
+
* deployed things. A resource in the root document keeps its bare id,
|
|
130
|
+
* which is also what CloudFormation shows for it.
|
|
131
|
+
*/
|
|
132
|
+
declare function qualifiedLogicalId(stackPath: string[], logicalId: string): string;
|
|
133
|
+
/** A line naming one child the reader could not open, and why. */
|
|
134
|
+
declare function unfollowedStackMessage(stack: UnfollowedStack): string;
|
|
135
|
+
|
|
47
136
|
/** A route-bearing SAM Event (`Type: Api` or `Type: HttpApi`). */
|
|
48
137
|
interface ServerlessHttpRoute {
|
|
49
138
|
/** Key of the event in the function's `Events` map. */
|
|
@@ -104,4 +193,4 @@ declare function parseHandler(handler: string): ParsedHandler | null;
|
|
|
104
193
|
*/
|
|
105
194
|
declare function readServerlessFunctions(template: CloudFormationTemplate): ServerlessFunctionInfo[];
|
|
106
195
|
|
|
107
|
-
export { CLOUDFORMATION_YAML_TAGS, type CloudFormationResource, type CloudFormationTemplate, type ParsedHandler, type ServerlessFunctionInfo, type ServerlessHttpRoute, type ServerlessNonHttpEvent, loadCloudFormationTemplate, parseHandler, readServerlessFunctions, refTarget };
|
|
196
|
+
export { type AppSyncResolverBinding, CLOUDFORMATION_YAML_TAGS, type CloudFormationResource, type CloudFormationTemplate, MAX_STACK_DEPTH, type ParsedHandler, type ServerlessFunctionInfo, type ServerlessHttpRoute, type ServerlessNonHttpEvent, type TemplateDocument, type TemplateTree, type UnfollowedReason, type UnfollowedStack, inheritedEnvVars, loadCloudFormationTemplate, loadTemplateTree, parseHandler, qualifiedLogicalId, readAppSyncResolvers, readServerlessFunctions, refTarget, resourcesWithGlobals, unfollowedStackMessage };
|
package/dist/index.js
CHANGED
|
@@ -2,17 +2,27 @@
|
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import YAML from "yaml";
|
|
5
|
+
var everyNodeKind = (tag, resolve) => {
|
|
6
|
+
const forNode = (value) => resolve(plainly(value));
|
|
7
|
+
return [
|
|
8
|
+
{ tag, resolve: forNode },
|
|
9
|
+
{ tag, collection: "seq", resolve: forNode },
|
|
10
|
+
{ tag, collection: "map", resolve: forNode }
|
|
11
|
+
];
|
|
12
|
+
};
|
|
13
|
+
var plainly = (value) => typeof value === "object" && value !== null && "toJSON" in value && typeof value.toJSON === "function" ? value.toJSON() : value;
|
|
14
|
+
var getAttParts = (value) => {
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
return value.map(String);
|
|
17
|
+
}
|
|
18
|
+
const text = String(value);
|
|
19
|
+
return text.includes(".") ? text.split(".") : [text];
|
|
20
|
+
};
|
|
5
21
|
var CLOUDFORMATION_YAML_TAGS = [
|
|
6
|
-
{
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
tag: "!GetAtt",
|
|
12
|
-
resolve: (value) => ({
|
|
13
|
-
"Fn::GetAtt": value.includes(".") ? value.split(".") : [value]
|
|
14
|
-
})
|
|
15
|
-
},
|
|
22
|
+
...everyNodeKind("!Ref", (value) => ({ Ref: value })),
|
|
23
|
+
...everyNodeKind("!GetAtt", (value) => ({
|
|
24
|
+
"Fn::GetAtt": getAttParts(value)
|
|
25
|
+
})),
|
|
16
26
|
...[
|
|
17
27
|
"!Sub",
|
|
18
28
|
"!Join",
|
|
@@ -27,7 +37,7 @@ var CLOUDFORMATION_YAML_TAGS = [
|
|
|
27
37
|
"!And",
|
|
28
38
|
"!Or",
|
|
29
39
|
"!Equals"
|
|
30
|
-
].
|
|
40
|
+
].flatMap((tag) => everyNodeKind(tag, (value) => value))
|
|
31
41
|
];
|
|
32
42
|
function loadCloudFormationTemplate(templatePath) {
|
|
33
43
|
const resolved = path.resolve(templatePath);
|
|
@@ -57,10 +67,393 @@ function refTarget(value) {
|
|
|
57
67
|
if (Array.isArray(getAtt) && typeof getAtt[0] === "string") {
|
|
58
68
|
return getAtt[0];
|
|
59
69
|
}
|
|
70
|
+
if (typeof getAtt === "string") {
|
|
71
|
+
const dot = getAtt.indexOf(".");
|
|
72
|
+
return dot === -1 ? getAtt : getAtt.slice(0, dot);
|
|
73
|
+
}
|
|
60
74
|
return null;
|
|
61
75
|
}
|
|
62
76
|
|
|
77
|
+
// src/appsyncResolvers.ts
|
|
78
|
+
var LAMBDA_CATEGORIES = ["Lambda", "Lambdas"];
|
|
79
|
+
function readAppSyncResolvers(template) {
|
|
80
|
+
const resources = template.Resources ?? {};
|
|
81
|
+
return [
|
|
82
|
+
...fromServerlessShorthand(resources),
|
|
83
|
+
...fromRawResources(resources)
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
function fromServerlessShorthand(resources) {
|
|
87
|
+
const bindings = [];
|
|
88
|
+
for (const resource of Object.values(resources)) {
|
|
89
|
+
if (resource?.Type !== "AWS::Serverless::GraphQLApi") {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const props = asRecord(resource.Properties) ?? {};
|
|
93
|
+
const lambdaByDataSource = shorthandDataSources(props.DataSources);
|
|
94
|
+
const dataSourceByFunction = shorthandFunctions(props.Functions);
|
|
95
|
+
for (const [typeName, fields] of entriesOf(props.Resolvers)) {
|
|
96
|
+
for (const [fieldName, config] of entriesOf(fields)) {
|
|
97
|
+
const dataSources = dataSourcesReached(config, dataSourceByFunction);
|
|
98
|
+
bindings.push({
|
|
99
|
+
typeName,
|
|
100
|
+
fieldName,
|
|
101
|
+
lambdaFunctionLogicalIds: dataSources.map((name) => lambdaByDataSource.get(name) ?? null).filter((id) => id !== null)
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return bindings;
|
|
107
|
+
}
|
|
108
|
+
function shorthandDataSources(block) {
|
|
109
|
+
const out = /* @__PURE__ */ new Map();
|
|
110
|
+
for (const [category, entries] of entriesOf(block)) {
|
|
111
|
+
if (!LAMBDA_CATEGORIES.includes(category)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
for (const [name, config] of entriesOf(entries)) {
|
|
115
|
+
const props = asRecord(config);
|
|
116
|
+
out.set(name, props === null ? null : refTarget(props.FunctionArn));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
function shorthandFunctions(block) {
|
|
122
|
+
const out = /* @__PURE__ */ new Map();
|
|
123
|
+
for (const [name, config] of entriesOf(block)) {
|
|
124
|
+
const props = asRecord(config);
|
|
125
|
+
const dataSource = props === null ? null : stringOf(props.DataSource);
|
|
126
|
+
if (dataSource !== null) {
|
|
127
|
+
out.set(name, dataSource);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
function dataSourcesReached(config, dataSourceByFunction) {
|
|
133
|
+
const props = asRecord(config);
|
|
134
|
+
if (props === null) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
const direct = stringOf(props.DataSource);
|
|
138
|
+
if (direct !== null) {
|
|
139
|
+
return [direct];
|
|
140
|
+
}
|
|
141
|
+
const pipeline = Array.isArray(props.Pipeline) ? props.Pipeline : [];
|
|
142
|
+
return pipeline.map((name) => dataSourceByFunction.get(String(name))).filter((name) => name !== void 0);
|
|
143
|
+
}
|
|
144
|
+
function fromRawResources(resources) {
|
|
145
|
+
const lambdaByLogicalId = /* @__PURE__ */ new Map();
|
|
146
|
+
const lambdaByApiAndName = /* @__PURE__ */ new Map();
|
|
147
|
+
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
148
|
+
if (resource?.Type !== "AWS::AppSync::DataSource") {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const props = asRecord(resource.Properties) ?? {};
|
|
152
|
+
const lambdaConfig = asRecord(props.LambdaConfig);
|
|
153
|
+
const lambda = lambdaConfig === null ? null : refTarget(lambdaConfig.LambdaFunctionArn);
|
|
154
|
+
if (lambda === null) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
lambdaByLogicalId.set(logicalId, lambda);
|
|
158
|
+
const name = stringOf(props.Name);
|
|
159
|
+
if (name !== null) {
|
|
160
|
+
lambdaByApiAndName.set(
|
|
161
|
+
apiScopedKey(refTarget(props.ApiId), name),
|
|
162
|
+
lambda
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const dataSourceByLogicalId = /* @__PURE__ */ new Map();
|
|
167
|
+
const dataSourceByApiAndName = /* @__PURE__ */ new Map();
|
|
168
|
+
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
169
|
+
if (resource?.Type !== "AWS::AppSync::FunctionConfiguration") {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const props = asRecord(resource.Properties) ?? {};
|
|
173
|
+
const dataSource = refTarget(props.DataSourceName);
|
|
174
|
+
if (dataSource === null) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
dataSourceByLogicalId.set(logicalId, dataSource);
|
|
178
|
+
const name = stringOf(props.Name);
|
|
179
|
+
if (name !== null) {
|
|
180
|
+
dataSourceByApiAndName.set(
|
|
181
|
+
apiScopedKey(refTarget(props.ApiId), name),
|
|
182
|
+
dataSource
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const bindings = [];
|
|
187
|
+
for (const resource of Object.values(resources)) {
|
|
188
|
+
if (resource?.Type !== "AWS::AppSync::Resolver") {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const props = asRecord(resource.Properties) ?? {};
|
|
192
|
+
const typeName = stringOf(props.TypeName);
|
|
193
|
+
const fieldName = stringOf(props.FieldName);
|
|
194
|
+
if (typeName === null || fieldName === null) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const apiId = refTarget(props.ApiId);
|
|
198
|
+
const lookup = (byLogicalId, byName) => (reference) => byLogicalId.get(reference) ?? byName.get(apiScopedKey(apiId, reference));
|
|
199
|
+
bindings.push({
|
|
200
|
+
typeName,
|
|
201
|
+
fieldName,
|
|
202
|
+
lambdaFunctionLogicalIds: rawDataSourcesReached(
|
|
203
|
+
props,
|
|
204
|
+
lookup(dataSourceByLogicalId, dataSourceByApiAndName)
|
|
205
|
+
).map(lookup(lambdaByLogicalId, lambdaByApiAndName)).filter((id) => id !== void 0)
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return bindings;
|
|
209
|
+
}
|
|
210
|
+
function apiScopedKey(apiId, name) {
|
|
211
|
+
return `${apiId ?? ""}::${name}`;
|
|
212
|
+
}
|
|
213
|
+
function rawDataSourcesReached(props, dataSourceOf) {
|
|
214
|
+
const direct = refTarget(props.DataSourceName);
|
|
215
|
+
if (direct !== null) {
|
|
216
|
+
return [direct];
|
|
217
|
+
}
|
|
218
|
+
const pipeline = asRecord(props.PipelineConfig);
|
|
219
|
+
const functions = Array.isArray(pipeline?.Functions) ? pipeline.Functions : [];
|
|
220
|
+
return functions.map((fn) => {
|
|
221
|
+
const reference = refTarget(fn);
|
|
222
|
+
return reference === null ? void 0 : dataSourceOf(reference);
|
|
223
|
+
}).filter((name) => name !== void 0);
|
|
224
|
+
}
|
|
225
|
+
function asRecord(value) {
|
|
226
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
227
|
+
}
|
|
228
|
+
function entriesOf(value) {
|
|
229
|
+
const record = asRecord(value);
|
|
230
|
+
return record === null ? [] : Object.entries(record);
|
|
231
|
+
}
|
|
232
|
+
function stringOf(value) {
|
|
233
|
+
return typeof value === "string" ? value : null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/globals.ts
|
|
237
|
+
var GLOBALS_SECTION_TYPES = {
|
|
238
|
+
Api: "AWS::Serverless::Api",
|
|
239
|
+
Function: "AWS::Serverless::Function",
|
|
240
|
+
HttpApi: "AWS::Serverless::HttpApi",
|
|
241
|
+
LayerVersion: "AWS::Serverless::LayerVersion",
|
|
242
|
+
SimpleTable: "AWS::Serverless::SimpleTable",
|
|
243
|
+
StateMachine: "AWS::Serverless::StateMachine"
|
|
244
|
+
};
|
|
245
|
+
function resourcesWithGlobals(template) {
|
|
246
|
+
const resources = template.Resources ?? {};
|
|
247
|
+
const defaultsByType = defaultsPerResourceType(template.Globals);
|
|
248
|
+
if (defaultsByType.size === 0) {
|
|
249
|
+
return resources;
|
|
250
|
+
}
|
|
251
|
+
const out = {};
|
|
252
|
+
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
253
|
+
const defaults = defaultsByType.get(resource.Type ?? "");
|
|
254
|
+
out[logicalId] = defaults === void 0 ? resource : {
|
|
255
|
+
...resource,
|
|
256
|
+
Properties: mergedProperties(defaults, resource.Properties ?? {})
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
function inheritedEnvVars(template) {
|
|
262
|
+
const sectionVars = envVarNames(template.Globals?.Function);
|
|
263
|
+
if (sectionVars.length === 0) {
|
|
264
|
+
return {};
|
|
265
|
+
}
|
|
266
|
+
const out = {};
|
|
267
|
+
for (const [logicalId, resource] of Object.entries(
|
|
268
|
+
template.Resources ?? {}
|
|
269
|
+
)) {
|
|
270
|
+
if (resource.Type !== GLOBALS_SECTION_TYPES.Function) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const own = new Set(envVarNames(resource.Properties));
|
|
274
|
+
out[logicalId] = sectionVars.filter((name) => !own.has(name));
|
|
275
|
+
}
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
278
|
+
function envVarNames(properties) {
|
|
279
|
+
const environment = properties?.Environment;
|
|
280
|
+
if (!isPlainObject(environment)) {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
const variables = environment.Variables;
|
|
284
|
+
return isPlainObject(variables) ? Object.keys(variables) : [];
|
|
285
|
+
}
|
|
286
|
+
function defaultsPerResourceType(globals) {
|
|
287
|
+
const byType = /* @__PURE__ */ new Map();
|
|
288
|
+
if (globals === null || typeof globals !== "object") {
|
|
289
|
+
return byType;
|
|
290
|
+
}
|
|
291
|
+
for (const [section, properties] of Object.entries(globals)) {
|
|
292
|
+
const resourceType = GLOBALS_SECTION_TYPES[section];
|
|
293
|
+
if (resourceType === void 0 || !isPlainObject(properties)) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
byType.set(resourceType, properties);
|
|
297
|
+
}
|
|
298
|
+
return byType;
|
|
299
|
+
}
|
|
300
|
+
function mergedProperties(defaults, own) {
|
|
301
|
+
const merged = { ...own };
|
|
302
|
+
for (const [name, fallback] of Object.entries(defaults)) {
|
|
303
|
+
merged[name] = name in own ? combined(fallback, own[name]) : fallback;
|
|
304
|
+
}
|
|
305
|
+
return merged;
|
|
306
|
+
}
|
|
307
|
+
function combined(fallback, own) {
|
|
308
|
+
if (Array.isArray(fallback) && Array.isArray(own)) {
|
|
309
|
+
return [...fallback, ...own];
|
|
310
|
+
}
|
|
311
|
+
if (isMergeableMap(fallback) && isMergeableMap(own)) {
|
|
312
|
+
return mergedProperties(fallback, own);
|
|
313
|
+
}
|
|
314
|
+
return own;
|
|
315
|
+
}
|
|
316
|
+
function isMergeableMap(value) {
|
|
317
|
+
return isPlainObject(value) && !isIntrinsic(value);
|
|
318
|
+
}
|
|
319
|
+
function isPlainObject(value) {
|
|
320
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
321
|
+
}
|
|
322
|
+
function isIntrinsic(value) {
|
|
323
|
+
const keys = Object.keys(value);
|
|
324
|
+
return keys.length === 1 && (keys[0] === "Ref" || keys[0].startsWith("Fn::") || keys[0] === "Condition");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/nestedStacks.ts
|
|
328
|
+
import fs2 from "fs";
|
|
329
|
+
import path2 from "path";
|
|
330
|
+
var CHILD_TEMPLATE_PROPERTY = {
|
|
331
|
+
"AWS::CloudFormation::Stack": "TemplateURL",
|
|
332
|
+
"AWS::Serverless::Application": "Location"
|
|
333
|
+
};
|
|
334
|
+
var MAX_STACK_DEPTH = 10;
|
|
335
|
+
function loadTemplateTree(rootPath) {
|
|
336
|
+
const rootFile = path2.resolve(rootPath);
|
|
337
|
+
const root = {
|
|
338
|
+
path: rootFile,
|
|
339
|
+
stackPath: [],
|
|
340
|
+
template: loadCloudFormationTemplate(rootFile)
|
|
341
|
+
};
|
|
342
|
+
const documents = [root];
|
|
343
|
+
const unfollowed = [];
|
|
344
|
+
const queue = [
|
|
345
|
+
{ document: root, ancestors: [rootFile] }
|
|
346
|
+
];
|
|
347
|
+
for (let i = 0; i < queue.length; i += 1) {
|
|
348
|
+
const { document, ancestors } = queue[i];
|
|
349
|
+
for (const child of childStacksOf(document, ancestors)) {
|
|
350
|
+
if (child.type === "unfollowed") {
|
|
351
|
+
unfollowed.push(child.stack);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
documents.push(child.document);
|
|
355
|
+
queue.push({
|
|
356
|
+
document: child.document,
|
|
357
|
+
ancestors: [...ancestors, child.document.path]
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return { documents, unfollowed };
|
|
362
|
+
}
|
|
363
|
+
function qualifiedLogicalId(stackPath, logicalId) {
|
|
364
|
+
return [...stackPath, logicalId].join("/");
|
|
365
|
+
}
|
|
366
|
+
function unfollowedStackMessage(stack) {
|
|
367
|
+
const name = stack.stackPath.join("/");
|
|
368
|
+
return `could not follow nested stack ${name} declared in ${stack.declaredIn}: ${stack.detail}`;
|
|
369
|
+
}
|
|
370
|
+
function childStacksOf(document, ancestors) {
|
|
371
|
+
const out = [];
|
|
372
|
+
for (const [logicalId, resource] of Object.entries(
|
|
373
|
+
document.template.Resources ?? {}
|
|
374
|
+
)) {
|
|
375
|
+
const property = CHILD_TEMPLATE_PROPERTY[resource.Type ?? ""];
|
|
376
|
+
if (property === void 0) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
out.push(
|
|
380
|
+
childStack({
|
|
381
|
+
document,
|
|
382
|
+
ancestors,
|
|
383
|
+
logicalId,
|
|
384
|
+
location: resource.Properties?.[property]
|
|
385
|
+
})
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
function childStack(opts) {
|
|
391
|
+
const stackPath = [...opts.document.stackPath, opts.logicalId];
|
|
392
|
+
const report = (reason, detail, templateUrl) => ({
|
|
393
|
+
type: "unfollowed",
|
|
394
|
+
stack: {
|
|
395
|
+
declaredIn: opts.document.path,
|
|
396
|
+
stackPath,
|
|
397
|
+
templateUrl,
|
|
398
|
+
reason,
|
|
399
|
+
detail
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
const url = typeof opts.location === "string" ? opts.location.trim() : null;
|
|
403
|
+
if (url === null || url === "") {
|
|
404
|
+
return report(
|
|
405
|
+
"notALiteralPath",
|
|
406
|
+
"the template location is not a literal string",
|
|
407
|
+
null
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
if (url.includes("${")) {
|
|
411
|
+
return report(
|
|
412
|
+
"notALiteralPath",
|
|
413
|
+
`${url} holds a substitution nothing here can fill in`,
|
|
414
|
+
url
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
if (hasUrlScheme(url)) {
|
|
418
|
+
return report("remoteUrl", `${url} is not a path in this repository`, url);
|
|
419
|
+
}
|
|
420
|
+
if (stackPath.length > MAX_STACK_DEPTH) {
|
|
421
|
+
return report(
|
|
422
|
+
"depthLimit",
|
|
423
|
+
`nesting is deeper than ${MAX_STACK_DEPTH} stacks`,
|
|
424
|
+
url
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
const childPath = path2.resolve(path2.dirname(opts.document.path), url);
|
|
428
|
+
if (opts.ancestors.includes(childPath)) {
|
|
429
|
+
return report("cycle", `${url} is already open further up the tree`, url);
|
|
430
|
+
}
|
|
431
|
+
if (!fs2.existsSync(childPath)) {
|
|
432
|
+
return report("fileMissing", `${url} is not on disk`, url);
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
return {
|
|
436
|
+
type: "document",
|
|
437
|
+
document: {
|
|
438
|
+
path: childPath,
|
|
439
|
+
stackPath,
|
|
440
|
+
template: loadCloudFormationTemplate(childPath)
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
} catch (err) {
|
|
444
|
+
return report(
|
|
445
|
+
"unreadable",
|
|
446
|
+
`${url} could not be parsed: ${err instanceof Error ? err.message : String(err)}`,
|
|
447
|
+
url
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function hasUrlScheme(location) {
|
|
452
|
+
return /^[a-z][a-z0-9+.-]*:\/\//i.test(location);
|
|
453
|
+
}
|
|
454
|
+
|
|
63
455
|
// src/serverlessFunctions.ts
|
|
456
|
+
import { codeScopePath } from "@suss/ir-core";
|
|
64
457
|
function parseHandler(handler) {
|
|
65
458
|
const trimmed = handler.trim();
|
|
66
459
|
const lastDot = trimmed.lastIndexOf(".");
|
|
@@ -72,24 +465,12 @@ function parseHandler(handler) {
|
|
|
72
465
|
exportName: trimmed.slice(lastDot + 1)
|
|
73
466
|
};
|
|
74
467
|
}
|
|
75
|
-
function
|
|
76
|
-
const trimmed = raw.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
77
|
-
return trimmed === "" ? "." : trimmed;
|
|
78
|
-
}
|
|
79
|
-
function readGlobalsCodeUri(template) {
|
|
80
|
-
const fnGlobals = template.Globals?.Function;
|
|
81
|
-
if (fnGlobals === void 0) {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
const codeUri = fnGlobals.CodeUri;
|
|
85
|
-
return typeof codeUri === "string" ? normalizeCodeUri(codeUri) : null;
|
|
86
|
-
}
|
|
87
|
-
function readCodeUri(resource, globalsCodeUri) {
|
|
468
|
+
function readCodeUri(resource) {
|
|
88
469
|
const codeUri = resource.Properties?.CodeUri;
|
|
89
|
-
if (typeof codeUri
|
|
90
|
-
return
|
|
470
|
+
if (typeof codeUri !== "string") {
|
|
471
|
+
return ".";
|
|
91
472
|
}
|
|
92
|
-
return
|
|
473
|
+
return codeScopePath(codeUri) || ".";
|
|
93
474
|
}
|
|
94
475
|
function classifyEvents(events) {
|
|
95
476
|
const httpRoutes = [];
|
|
@@ -109,8 +490,8 @@ function classifyEvents(events) {
|
|
|
109
490
|
}
|
|
110
491
|
const props = event.Properties ?? {};
|
|
111
492
|
const method = String(props.Method ?? "").toUpperCase();
|
|
112
|
-
const
|
|
113
|
-
if (method === "" ||
|
|
493
|
+
const path3 = String(props.Path ?? "");
|
|
494
|
+
if (method === "" || path3 === "") {
|
|
114
495
|
nonHttpEvents.push({ eventId, eventType: type });
|
|
115
496
|
continue;
|
|
116
497
|
}
|
|
@@ -119,15 +500,14 @@ function classifyEvents(events) {
|
|
|
119
500
|
eventId,
|
|
120
501
|
eventType: type,
|
|
121
502
|
method,
|
|
122
|
-
path:
|
|
503
|
+
path: path3,
|
|
123
504
|
apiId: apiRef
|
|
124
505
|
});
|
|
125
506
|
}
|
|
126
507
|
return { httpRoutes, nonHttpEvents };
|
|
127
508
|
}
|
|
128
509
|
function readServerlessFunctions(template) {
|
|
129
|
-
const resources = template
|
|
130
|
-
const globalsCodeUri = readGlobalsCodeUri(template);
|
|
510
|
+
const resources = resourcesWithGlobals(template);
|
|
131
511
|
const out = [];
|
|
132
512
|
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
133
513
|
if (resource.Type !== "AWS::Serverless::Function") {
|
|
@@ -149,7 +529,7 @@ function readServerlessFunctions(template) {
|
|
|
149
529
|
handler: handlerRaw,
|
|
150
530
|
modulePath: parsed.modulePath,
|
|
151
531
|
exportName: parsed.exportName,
|
|
152
|
-
codeUri: readCodeUri(resource
|
|
532
|
+
codeUri: readCodeUri(resource),
|
|
153
533
|
httpRoutes,
|
|
154
534
|
nonHttpEvents
|
|
155
535
|
});
|
|
@@ -158,8 +538,15 @@ function readServerlessFunctions(template) {
|
|
|
158
538
|
}
|
|
159
539
|
export {
|
|
160
540
|
CLOUDFORMATION_YAML_TAGS,
|
|
541
|
+
MAX_STACK_DEPTH,
|
|
542
|
+
inheritedEnvVars,
|
|
161
543
|
loadCloudFormationTemplate,
|
|
544
|
+
loadTemplateTree,
|
|
162
545
|
parseHandler,
|
|
546
|
+
qualifiedLogicalId,
|
|
547
|
+
readAppSyncResolvers,
|
|
163
548
|
readServerlessFunctions,
|
|
164
|
-
refTarget
|
|
549
|
+
refTarget,
|
|
550
|
+
resourcesWithGlobals,
|
|
551
|
+
unfollowedStackMessage
|
|
165
552
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suss/manifest-aws",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Parse AWS CloudFormation / SAM templates into plain data — the shared facts layer contract readers and manifest-driven framework packs both consume.",
|
|
6
6
|
"keywords": [
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"typecheck": "tsc --noEmit"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
+
"@suss/ir-core": "0.1.1",
|
|
48
49
|
"yaml": "^2.4.0"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|