@suss/manifest-aws 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +268 -35
- package/dist/index.d.cts +87 -16
- package/dist/index.d.ts +87 -16
- package/dist/index.js +261 -34
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -31,11 +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,
|
|
36
40
|
readAppSyncResolvers: () => readAppSyncResolvers,
|
|
37
41
|
readServerlessFunctions: () => readServerlessFunctions,
|
|
38
|
-
refTarget: () => refTarget
|
|
42
|
+
refTarget: () => refTarget,
|
|
43
|
+
resourcesWithGlobals: () => resourcesWithGlobals,
|
|
44
|
+
unfollowedStackMessage: () => unfollowedStackMessage
|
|
39
45
|
});
|
|
40
46
|
module.exports = __toCommonJS(index_exports);
|
|
41
47
|
|
|
@@ -43,17 +49,27 @@ module.exports = __toCommonJS(index_exports);
|
|
|
43
49
|
var import_node_fs = __toESM(require("fs"), 1);
|
|
44
50
|
var import_node_path = __toESM(require("path"), 1);
|
|
45
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
|
+
};
|
|
46
68
|
var CLOUDFORMATION_YAML_TAGS = [
|
|
47
|
-
{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
tag: "!GetAtt",
|
|
53
|
-
resolve: (value) => ({
|
|
54
|
-
"Fn::GetAtt": value.includes(".") ? value.split(".") : [value]
|
|
55
|
-
})
|
|
56
|
-
},
|
|
69
|
+
...everyNodeKind("!Ref", (value) => ({ Ref: value })),
|
|
70
|
+
...everyNodeKind("!GetAtt", (value) => ({
|
|
71
|
+
"Fn::GetAtt": getAttParts(value)
|
|
72
|
+
})),
|
|
57
73
|
...[
|
|
58
74
|
"!Sub",
|
|
59
75
|
"!Join",
|
|
@@ -68,7 +84,7 @@ var CLOUDFORMATION_YAML_TAGS = [
|
|
|
68
84
|
"!And",
|
|
69
85
|
"!Or",
|
|
70
86
|
"!Equals"
|
|
71
|
-
].
|
|
87
|
+
].flatMap((tag) => everyNodeKind(tag, (value) => value))
|
|
72
88
|
];
|
|
73
89
|
function loadCloudFormationTemplate(templatePath) {
|
|
74
90
|
const resolved = import_node_path.default.resolve(templatePath);
|
|
@@ -98,6 +114,10 @@ function refTarget(value) {
|
|
|
98
114
|
if (Array.isArray(getAtt) && typeof getAtt[0] === "string") {
|
|
99
115
|
return getAtt[0];
|
|
100
116
|
}
|
|
117
|
+
if (typeof getAtt === "string") {
|
|
118
|
+
const dot = getAtt.indexOf(".");
|
|
119
|
+
return dot === -1 ? getAtt : getAtt.slice(0, dot);
|
|
120
|
+
}
|
|
101
121
|
return null;
|
|
102
122
|
}
|
|
103
123
|
|
|
@@ -260,7 +280,227 @@ function stringOf(value) {
|
|
|
260
280
|
return typeof value === "string" ? value : null;
|
|
261
281
|
}
|
|
262
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
|
+
|
|
263
502
|
// src/serverlessFunctions.ts
|
|
503
|
+
var import_ir_core = require("@suss/ir-core");
|
|
264
504
|
function parseHandler(handler) {
|
|
265
505
|
const trimmed = handler.trim();
|
|
266
506
|
const lastDot = trimmed.lastIndexOf(".");
|
|
@@ -272,24 +512,12 @@ function parseHandler(handler) {
|
|
|
272
512
|
exportName: trimmed.slice(lastDot + 1)
|
|
273
513
|
};
|
|
274
514
|
}
|
|
275
|
-
function
|
|
276
|
-
const trimmed = raw.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
277
|
-
return trimmed === "" ? "." : trimmed;
|
|
278
|
-
}
|
|
279
|
-
function readGlobalsCodeUri(template) {
|
|
280
|
-
const fnGlobals = template.Globals?.Function;
|
|
281
|
-
if (fnGlobals === void 0) {
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
const codeUri = fnGlobals.CodeUri;
|
|
285
|
-
return typeof codeUri === "string" ? normalizeCodeUri(codeUri) : null;
|
|
286
|
-
}
|
|
287
|
-
function readCodeUri(resource, globalsCodeUri) {
|
|
515
|
+
function readCodeUri(resource) {
|
|
288
516
|
const codeUri = resource.Properties?.CodeUri;
|
|
289
|
-
if (typeof codeUri
|
|
290
|
-
return
|
|
517
|
+
if (typeof codeUri !== "string") {
|
|
518
|
+
return ".";
|
|
291
519
|
}
|
|
292
|
-
return
|
|
520
|
+
return (0, import_ir_core.codeScopePath)(codeUri) || ".";
|
|
293
521
|
}
|
|
294
522
|
function classifyEvents(events) {
|
|
295
523
|
const httpRoutes = [];
|
|
@@ -309,8 +537,8 @@ function classifyEvents(events) {
|
|
|
309
537
|
}
|
|
310
538
|
const props = event.Properties ?? {};
|
|
311
539
|
const method = String(props.Method ?? "").toUpperCase();
|
|
312
|
-
const
|
|
313
|
-
if (method === "" ||
|
|
540
|
+
const path3 = String(props.Path ?? "");
|
|
541
|
+
if (method === "" || path3 === "") {
|
|
314
542
|
nonHttpEvents.push({ eventId, eventType: type });
|
|
315
543
|
continue;
|
|
316
544
|
}
|
|
@@ -319,15 +547,14 @@ function classifyEvents(events) {
|
|
|
319
547
|
eventId,
|
|
320
548
|
eventType: type,
|
|
321
549
|
method,
|
|
322
|
-
path:
|
|
550
|
+
path: path3,
|
|
323
551
|
apiId: apiRef
|
|
324
552
|
});
|
|
325
553
|
}
|
|
326
554
|
return { httpRoutes, nonHttpEvents };
|
|
327
555
|
}
|
|
328
556
|
function readServerlessFunctions(template) {
|
|
329
|
-
const resources = template
|
|
330
|
-
const globalsCodeUri = readGlobalsCodeUri(template);
|
|
557
|
+
const resources = resourcesWithGlobals(template);
|
|
331
558
|
const out = [];
|
|
332
559
|
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
333
560
|
if (resource.Type !== "AWS::Serverless::Function") {
|
|
@@ -349,7 +576,7 @@ function readServerlessFunctions(template) {
|
|
|
349
576
|
handler: handlerRaw,
|
|
350
577
|
modulePath: parsed.modulePath,
|
|
351
578
|
exportName: parsed.exportName,
|
|
352
|
-
codeUri: readCodeUri(resource
|
|
579
|
+
codeUri: readCodeUri(resource),
|
|
353
580
|
httpRoutes,
|
|
354
581
|
nonHttpEvents
|
|
355
582
|
});
|
|
@@ -359,9 +586,15 @@ function readServerlessFunctions(template) {
|
|
|
359
586
|
// Annotate the CommonJS export names for ESM import in node:
|
|
360
587
|
0 && (module.exports = {
|
|
361
588
|
CLOUDFORMATION_YAML_TAGS,
|
|
589
|
+
MAX_STACK_DEPTH,
|
|
590
|
+
inheritedEnvVars,
|
|
362
591
|
loadCloudFormationTemplate,
|
|
592
|
+
loadTemplateTree,
|
|
363
593
|
parseHandler,
|
|
594
|
+
qualifiedLogicalId,
|
|
364
595
|
readAppSyncResolvers,
|
|
365
596
|
readServerlessFunctions,
|
|
366
|
-
refTarget
|
|
597
|
+
refTarget,
|
|
598
|
+
resourcesWithGlobals,
|
|
599
|
+
unfollowedStackMessage
|
|
367
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,10 +26,16 @@ 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
|
|
|
@@ -62,6 +57,82 @@ interface AppSyncResolverBinding {
|
|
|
62
57
|
}
|
|
63
58
|
declare function readAppSyncResolvers(template: CloudFormationTemplate): AppSyncResolverBinding[];
|
|
64
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
|
+
|
|
65
136
|
/** A route-bearing SAM Event (`Type: Api` or `Type: HttpApi`). */
|
|
66
137
|
interface ServerlessHttpRoute {
|
|
67
138
|
/** Key of the event in the function's `Events` map. */
|
|
@@ -122,4 +193,4 @@ declare function parseHandler(handler: string): ParsedHandler | null;
|
|
|
122
193
|
*/
|
|
123
194
|
declare function readServerlessFunctions(template: CloudFormationTemplate): ServerlessFunctionInfo[];
|
|
124
195
|
|
|
125
|
-
export { type AppSyncResolverBinding, CLOUDFORMATION_YAML_TAGS, type CloudFormationResource, type CloudFormationTemplate, type ParsedHandler, type ServerlessFunctionInfo, type ServerlessHttpRoute, type ServerlessNonHttpEvent, loadCloudFormationTemplate, parseHandler, readAppSyncResolvers, 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,10 +26,16 @@ 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
|
|
|
@@ -62,6 +57,82 @@ interface AppSyncResolverBinding {
|
|
|
62
57
|
}
|
|
63
58
|
declare function readAppSyncResolvers(template: CloudFormationTemplate): AppSyncResolverBinding[];
|
|
64
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
|
+
|
|
65
136
|
/** A route-bearing SAM Event (`Type: Api` or `Type: HttpApi`). */
|
|
66
137
|
interface ServerlessHttpRoute {
|
|
67
138
|
/** Key of the event in the function's `Events` map. */
|
|
@@ -122,4 +193,4 @@ declare function parseHandler(handler: string): ParsedHandler | null;
|
|
|
122
193
|
*/
|
|
123
194
|
declare function readServerlessFunctions(template: CloudFormationTemplate): ServerlessFunctionInfo[];
|
|
124
195
|
|
|
125
|
-
export { type AppSyncResolverBinding, CLOUDFORMATION_YAML_TAGS, type CloudFormationResource, type CloudFormationTemplate, type ParsedHandler, type ServerlessFunctionInfo, type ServerlessHttpRoute, type ServerlessNonHttpEvent, loadCloudFormationTemplate, parseHandler, readAppSyncResolvers, 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,6 +67,10 @@ 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
|
|
|
@@ -219,7 +233,227 @@ function stringOf(value) {
|
|
|
219
233
|
return typeof value === "string" ? value : null;
|
|
220
234
|
}
|
|
221
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
|
+
|
|
222
455
|
// src/serverlessFunctions.ts
|
|
456
|
+
import { codeScopePath } from "@suss/ir-core";
|
|
223
457
|
function parseHandler(handler) {
|
|
224
458
|
const trimmed = handler.trim();
|
|
225
459
|
const lastDot = trimmed.lastIndexOf(".");
|
|
@@ -231,24 +465,12 @@ function parseHandler(handler) {
|
|
|
231
465
|
exportName: trimmed.slice(lastDot + 1)
|
|
232
466
|
};
|
|
233
467
|
}
|
|
234
|
-
function
|
|
235
|
-
const trimmed = raw.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
236
|
-
return trimmed === "" ? "." : trimmed;
|
|
237
|
-
}
|
|
238
|
-
function readGlobalsCodeUri(template) {
|
|
239
|
-
const fnGlobals = template.Globals?.Function;
|
|
240
|
-
if (fnGlobals === void 0) {
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
|
-
const codeUri = fnGlobals.CodeUri;
|
|
244
|
-
return typeof codeUri === "string" ? normalizeCodeUri(codeUri) : null;
|
|
245
|
-
}
|
|
246
|
-
function readCodeUri(resource, globalsCodeUri) {
|
|
468
|
+
function readCodeUri(resource) {
|
|
247
469
|
const codeUri = resource.Properties?.CodeUri;
|
|
248
|
-
if (typeof codeUri
|
|
249
|
-
return
|
|
470
|
+
if (typeof codeUri !== "string") {
|
|
471
|
+
return ".";
|
|
250
472
|
}
|
|
251
|
-
return
|
|
473
|
+
return codeScopePath(codeUri) || ".";
|
|
252
474
|
}
|
|
253
475
|
function classifyEvents(events) {
|
|
254
476
|
const httpRoutes = [];
|
|
@@ -268,8 +490,8 @@ function classifyEvents(events) {
|
|
|
268
490
|
}
|
|
269
491
|
const props = event.Properties ?? {};
|
|
270
492
|
const method = String(props.Method ?? "").toUpperCase();
|
|
271
|
-
const
|
|
272
|
-
if (method === "" ||
|
|
493
|
+
const path3 = String(props.Path ?? "");
|
|
494
|
+
if (method === "" || path3 === "") {
|
|
273
495
|
nonHttpEvents.push({ eventId, eventType: type });
|
|
274
496
|
continue;
|
|
275
497
|
}
|
|
@@ -278,15 +500,14 @@ function classifyEvents(events) {
|
|
|
278
500
|
eventId,
|
|
279
501
|
eventType: type,
|
|
280
502
|
method,
|
|
281
|
-
path:
|
|
503
|
+
path: path3,
|
|
282
504
|
apiId: apiRef
|
|
283
505
|
});
|
|
284
506
|
}
|
|
285
507
|
return { httpRoutes, nonHttpEvents };
|
|
286
508
|
}
|
|
287
509
|
function readServerlessFunctions(template) {
|
|
288
|
-
const resources = template
|
|
289
|
-
const globalsCodeUri = readGlobalsCodeUri(template);
|
|
510
|
+
const resources = resourcesWithGlobals(template);
|
|
290
511
|
const out = [];
|
|
291
512
|
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
292
513
|
if (resource.Type !== "AWS::Serverless::Function") {
|
|
@@ -308,7 +529,7 @@ function readServerlessFunctions(template) {
|
|
|
308
529
|
handler: handlerRaw,
|
|
309
530
|
modulePath: parsed.modulePath,
|
|
310
531
|
exportName: parsed.exportName,
|
|
311
|
-
codeUri: readCodeUri(resource
|
|
532
|
+
codeUri: readCodeUri(resource),
|
|
312
533
|
httpRoutes,
|
|
313
534
|
nonHttpEvents
|
|
314
535
|
});
|
|
@@ -317,9 +538,15 @@ function readServerlessFunctions(template) {
|
|
|
317
538
|
}
|
|
318
539
|
export {
|
|
319
540
|
CLOUDFORMATION_YAML_TAGS,
|
|
541
|
+
MAX_STACK_DEPTH,
|
|
542
|
+
inheritedEnvVars,
|
|
320
543
|
loadCloudFormationTemplate,
|
|
544
|
+
loadTemplateTree,
|
|
321
545
|
parseHandler,
|
|
546
|
+
qualifiedLogicalId,
|
|
322
547
|
readAppSyncResolvers,
|
|
323
548
|
readServerlessFunctions,
|
|
324
|
-
refTarget
|
|
549
|
+
refTarget,
|
|
550
|
+
resourcesWithGlobals,
|
|
551
|
+
unfollowedStackMessage
|
|
325
552
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suss/manifest-aws",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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.2.0",
|
|
48
49
|
"yaml": "^2.4.0"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|