@suss/manifest-aws 0.35.1 → 0.37.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 CHANGED
@@ -32,14 +32,18 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  CLOUDFORMATION_YAML_TAGS: () => CLOUDFORMATION_YAML_TAGS,
34
34
  MAX_STACK_DEPTH: () => MAX_STACK_DEPTH,
35
+ apiScopedKey: () => apiScopedKey,
36
+ documentRef: () => documentRef,
35
37
  inheritedEnvVars: () => inheritedEnvVars,
38
+ loadCloudFormationDocument: () => loadCloudFormationDocument,
36
39
  loadCloudFormationTemplate: () => loadCloudFormationTemplate,
37
40
  loadTemplateTree: () => loadTemplateTree,
38
41
  parseHandler: () => import_ir_core2.parseHandler,
39
42
  qualifiedLogicalId: () => qualifiedLogicalId,
40
43
  readAppSyncResolvers: () => readAppSyncResolvers,
44
+ readEventPattern: () => readEventPattern,
45
+ readResourceLines: () => readResourceLines,
41
46
  readServerlessFunctions: () => readServerlessFunctions,
42
- reduceEventPattern: () => reduceEventPattern,
43
47
  refTarget: () => refTarget,
44
48
  resolveBucketChannel: () => resolveBucketChannel,
45
49
  resolveEventBusToken: () => resolveEventBusToken,
@@ -97,6 +101,9 @@ var CLOUDFORMATION_YAML_TAGS = [
97
101
  ].flatMap((tag) => everyNodeKind(tag, (value) => value))
98
102
  ];
99
103
  function loadCloudFormationTemplate(templatePath) {
104
+ return loadCloudFormationDocument(templatePath).template;
105
+ }
106
+ function loadCloudFormationDocument(templatePath) {
100
107
  const resolved = import_node_path.default.resolve(templatePath);
101
108
  if (!import_node_fs.default.existsSync(resolved)) {
102
109
  throw new Error(`CloudFormation template not found: ${resolved}`);
@@ -107,7 +114,30 @@ function loadCloudFormationTemplate(templatePath) {
107
114
  if (parsed === null || typeof parsed !== "object") {
108
115
  throw new Error(`CloudFormation template is not an object: ${resolved}`);
109
116
  }
110
- return parsed;
117
+ return {
118
+ template: parsed,
119
+ resourceLines: readResourceLines(raw)
120
+ };
121
+ }
122
+ function readResourceLines(raw) {
123
+ const lines = /* @__PURE__ */ new Map();
124
+ const lineCounter = new import_yaml.LineCounter();
125
+ const document = import_yaml.default.parseDocument(raw, {
126
+ customTags: CLOUDFORMATION_YAML_TAGS,
127
+ lineCounter
128
+ });
129
+ const resources = document.get("Resources", true);
130
+ if (!(0, import_yaml.isMap)(resources)) {
131
+ return lines;
132
+ }
133
+ for (const item of resources.items) {
134
+ const key = item.key;
135
+ if (!(0, import_yaml.isScalar)(key) || key.range === void 0 || key.range === null) {
136
+ continue;
137
+ }
138
+ lines.set(String(key.value), lineCounter.linePos(key.range[0]).line);
139
+ }
140
+ return lines;
111
141
  }
112
142
  function refTarget(value) {
113
143
  if (typeof value === "string") {
@@ -332,40 +362,36 @@ function resolveBucketChannel(value) {
332
362
  }
333
363
 
334
364
  // src/eventPattern.ts
335
- function reduceEventPattern(pattern) {
365
+ var STATED_FIELDS = /* @__PURE__ */ new Set(["detail-type", "source"]);
366
+ function readEventPattern(pattern) {
336
367
  if (pattern === null || typeof pattern !== "object") {
337
- return {
338
- kind: "unresolvable",
339
- reason: "rule declares no EventPattern"
340
- };
341
- }
342
- const detailType = pattern["detail-type"];
343
- if (detailType === void 0) {
344
- return {
345
- kind: "unresolvable",
346
- reason: "EventPattern has no detail-type; v0 reduces routing to exact detail-type match only"
347
- };
348
- }
349
- if (!Array.isArray(detailType)) {
350
- return {
351
- kind: "unresolvable",
352
- reason: "detail-type is not a plain array of string literals"
353
- };
368
+ return null;
354
369
  }
355
- const detailTypes = [];
356
- for (const entry of detailType) {
357
- if (typeof entry !== "string") {
358
- return {
359
- kind: "unresolvable",
360
- reason: "detail-type contains a content filter (prefix / anything-but / etc.); pattern subsumption is out of v0 scope"
361
- };
362
- }
363
- detailTypes.push(entry);
370
+ const record = pattern;
371
+ const detailTypes = literalValues(record["detail-type"]);
372
+ const sources = literalValues(record.source);
373
+ const unsettled = [
374
+ ...detailTypes === "unsettled" ? ["detail-type"] : [],
375
+ ...sources === "unsettled" ? ["source"] : []
376
+ ];
377
+ return {
378
+ ...Array.isArray(detailTypes) ? { detailTypes } : {},
379
+ ...Array.isArray(sources) ? { sources } : {},
380
+ unsettled,
381
+ filtersOtherFields: Object.keys(record).some(
382
+ (key) => !STATED_FIELDS.has(key)
383
+ ),
384
+ emittedByAws: Array.isArray(sources) && sources.some((source) => source.startsWith("aws."))
385
+ };
386
+ }
387
+ function literalValues(value) {
388
+ if (value === void 0) {
389
+ return null;
364
390
  }
365
- if (detailTypes.length === 0) {
366
- return { kind: "unresolvable", reason: "detail-type array is empty" };
391
+ if (!Array.isArray(value) || value.length === 0 || !value.every((entry) => typeof entry === "string")) {
392
+ return "unsettled";
367
393
  }
368
- return { kind: "exact", detailTypes };
394
+ return value;
369
395
  }
370
396
  function resolveEventBusToken(value) {
371
397
  if (value === null || value === void 0) {
@@ -484,12 +510,40 @@ var CHILD_TEMPLATE_PROPERTY = {
484
510
  "AWS::Serverless::Application": "Location"
485
511
  };
486
512
  var MAX_STACK_DEPTH = 10;
513
+ function documentRef(document, value) {
514
+ const id = refTarget(value);
515
+ if (id === null) {
516
+ return null;
517
+ }
518
+ return document.parameterTargets.get(id) ?? qualifiedLogicalId(document.stackPath, id);
519
+ }
520
+ function parameterTargetsOf(parent, passed) {
521
+ const targets = /* @__PURE__ */ new Map();
522
+ if (passed === null || typeof passed !== "object") {
523
+ return targets;
524
+ }
525
+ const resources = parent.template.Resources ?? {};
526
+ for (const [name, value] of Object.entries(
527
+ passed
528
+ )) {
529
+ const id = refTarget(value);
530
+ if (id === null || typeof value === "string") {
531
+ continue;
532
+ }
533
+ const target = parent.parameterTargets.get(id) ?? (resources[id] === void 0 ? null : qualifiedLogicalId(parent.stackPath, id));
534
+ if (target !== null) {
535
+ targets.set(name, target);
536
+ }
537
+ }
538
+ return targets;
539
+ }
487
540
  function loadTemplateTree(rootPath) {
488
541
  const rootFile = import_node_path2.default.resolve(rootPath);
489
542
  const root = {
490
543
  path: rootFile,
491
544
  stackPath: [],
492
- template: loadCloudFormationTemplate(rootFile)
545
+ ...loadCloudFormationDocument(rootFile),
546
+ parameterTargets: /* @__PURE__ */ new Map()
493
547
  };
494
548
  const documents = [root];
495
549
  const unfollowed = [];
@@ -533,7 +587,8 @@ function childStacksOf(document, ancestors) {
533
587
  document,
534
588
  ancestors,
535
589
  logicalId,
536
- location: resource.Properties?.[property]
590
+ location: resource.Properties?.[property],
591
+ parameters: resource.Properties?.Parameters
537
592
  })
538
593
  );
539
594
  }
@@ -595,7 +650,8 @@ function childStack(opts) {
595
650
  document: {
596
651
  path: childPath,
597
652
  stackPath,
598
- template: loadCloudFormationTemplate(childPath)
653
+ ...loadCloudFormationDocument(childPath),
654
+ parameterTargets: parameterTargetsOf(opts.document, opts.parameters)
599
655
  }
600
656
  };
601
657
  } catch (err) {
@@ -666,7 +722,9 @@ function readServerlessFunctions(template) {
666
722
  if (typeof handlerRaw !== "string") {
667
723
  continue;
668
724
  }
669
- const parsed = (0, import_ir_core.parseHandler)(handlerRaw);
725
+ const runtimeRaw = resource.Properties?.Runtime;
726
+ const runtime = typeof runtimeRaw === "string" ? runtimeRaw : void 0;
727
+ const parsed = (0, import_ir_core.parseHandler)(handlerRaw, runtime);
670
728
  if (parsed === null) {
671
729
  continue;
672
730
  }
@@ -679,6 +737,7 @@ function readServerlessFunctions(template) {
679
737
  modulePath: parsed.modulePath,
680
738
  exportName: parsed.exportName,
681
739
  codeUri: readCodeUri(resource),
740
+ ...runtime !== void 0 ? { runtime } : {},
682
741
  httpRoutes,
683
742
  nonHttpEvents
684
743
  });
@@ -689,14 +748,18 @@ function readServerlessFunctions(template) {
689
748
  0 && (module.exports = {
690
749
  CLOUDFORMATION_YAML_TAGS,
691
750
  MAX_STACK_DEPTH,
751
+ apiScopedKey,
752
+ documentRef,
692
753
  inheritedEnvVars,
754
+ loadCloudFormationDocument,
693
755
  loadCloudFormationTemplate,
694
756
  loadTemplateTree,
695
757
  parseHandler,
696
758
  qualifiedLogicalId,
697
759
  readAppSyncResolvers,
760
+ readEventPattern,
761
+ readResourceLines,
698
762
  readServerlessFunctions,
699
- reduceEventPattern,
700
763
  refTarget,
701
764
  resolveBucketChannel,
702
765
  resolveEventBusToken,
package/dist/index.d.cts CHANGED
@@ -35,6 +35,19 @@ declare const CLOUDFORMATION_YAML_TAGS: YAML.CollectionTag[];
35
35
  * malformed manifest is a load-time error rather than an empty result.
36
36
  */
37
37
  declare function loadCloudFormationTemplate(templatePath: string): CloudFormationTemplate;
38
+ interface LoadedTemplate {
39
+ template: CloudFormationTemplate;
40
+ /** The 1-based line each resource's logical id is written on. */
41
+ resourceLines: Map<string, number>;
42
+ }
43
+ /** `loadCloudFormationTemplate`, plus the line each resource starts on. */
44
+ declare function loadCloudFormationDocument(templatePath: string): LoadedTemplate;
45
+ /**
46
+ * The line each key under `Resources` is written on. JSON is valid YAML,
47
+ * so one reader covers both formats. A text that does not parse gives
48
+ * an empty map, and its summaries keep line 1.
49
+ */
50
+ declare function readResourceLines(raw: string): Map<string, number>;
38
51
  /**
39
52
  * CloudFormation references come out of the parser in four forms:
40
53
  * - { Ref: "LogicalId" }
@@ -81,6 +94,8 @@ interface AppSyncResolverBinding {
81
94
  lambdaFunctionLogicalIds: string[];
82
95
  }
83
96
  declare function readAppSyncResolvers(template: CloudFormationTemplate): AppSyncResolverBinding[];
97
+ /** A name only means something next to the API it was declared in. */
98
+ declare function apiScopedKey(apiId: string | null, name: string): string;
84
99
 
85
100
  /**
86
101
  * Resolves the references AWS manifests use to point at a queue, a topic,
@@ -128,25 +143,32 @@ declare function resolveBucketChannel(value: unknown): string | null;
128
143
  * CloudFormation template and in a serverless.yml eventBridge event, so
129
144
  * both readers reduce them here.
130
145
  */
146
+ /** A pattern field that is not a plain list of strings. */
147
+ type UnsettledPatternField = "detail-type" | "source";
131
148
  /**
132
- * How far a rule's EventPattern reduced: either to the exact
133
- * detail-types it lists, or not at all, along with a reason someone can
134
- * read.
149
+ * What a rule's EventPattern says about the two fields a send states.
150
+ * A field the pattern leaves out matches every value, so it has no
151
+ * entry here. A field written as anything but a list of strings, such
152
+ * as a prefix filter, is listed in `unsettled`.
135
153
  */
136
- type PatternReduction = {
137
- kind: "exact";
138
- detailTypes: string[];
139
- } | {
140
- kind: "unresolvable";
141
- reason: string;
142
- };
143
- /**
144
- * Reduce a rule's EventPattern to the exact set of DetailTypes it
145
- * matches. Only a literal `detail-type` array reduces. Anything else,
146
- * such as a missing detail-type, a content filter or an empty array,
147
- * comes back unresolvable with a reason, and the checker reports it.
148
- */
149
- declare function reduceEventPattern(pattern: unknown): PatternReduction;
154
+ interface EventPatternFields {
155
+ detailTypes?: string[];
156
+ sources?: string[];
157
+ unsettled: UnsettledPatternField[];
158
+ /**
159
+ * The pattern also filters on a field a send does not state, such as
160
+ * `detail`, so a send that agrees on source and detail-type still
161
+ * reaches the rule only for some events.
162
+ */
163
+ filtersOtherFields: boolean;
164
+ /**
165
+ * A source starts with `aws.`. Only AWS services send those, and
166
+ * PutEvents refuses them from anyone else.
167
+ */
168
+ emittedByAws: boolean;
169
+ }
170
+ /** Null when the rule declares no EventPattern at all. */
171
+ declare function readEventPattern(pattern: unknown): EventPatternFields | null;
150
172
  /**
151
173
  * Resolve a rule's EventBusName to the channel's bus token: the CFN
152
174
  * logical id when it's a Ref/GetAtt, the name segment of an event-bus
@@ -217,7 +239,22 @@ interface TemplateDocument {
217
239
  */
218
240
  stackPath: string[];
219
241
  template: CloudFormationTemplate;
242
+ /** The 1-based line each resource's logical id is written on. */
243
+ resourceLines: Map<string, number>;
244
+ /**
245
+ * The resource each of this document's parameters points at, for a
246
+ * parameter the parent stack sets to a reference. The id is qualified
247
+ * by its own document's stack path, so a child's `!Ref ApiId` can be
248
+ * read as the API its parent declares. Empty for the root.
249
+ */
250
+ parameterTargets: Map<string, string>;
220
251
  }
252
+ /**
253
+ * The qualified logical id a reference in this document points at: the
254
+ * resource the parent passed in as a parameter, or the document's own.
255
+ * Null for a value only known at deploy time.
256
+ */
257
+ declare function documentRef(document: Pick<TemplateDocument, "stackPath" | "parameterTargets">, value: unknown): string | null;
221
258
  /**
222
259
  * Why a child template was not read. Every value is a limit on what the
223
260
  * reader could reach, so a consumer should report it as a read that
@@ -309,15 +346,17 @@ interface ServerlessFunctionInfo {
309
346
  logicalId: string;
310
347
  /** Raw Handler string, e.g. "src/handlers/confirmToken.handler". */
311
348
  handler: string;
312
- /** Module-path portion of the handler (everything before the final dot). */
349
+ /** Module-path portion of the handler, as `parseHandler` splits it. */
313
350
  modulePath: string;
314
- /** Exported symbol the handler points at (everything after the final dot). */
351
+ /** The function the handler points at, as `parseHandler` splits it. */
315
352
  exportName: string;
316
353
  /**
317
354
  * CodeUri base directory the handler resolves against: the function's
318
355
  * own CodeUri, else the `Globals.Function.CodeUri` default, else ".".
319
356
  */
320
357
  codeUri: string;
358
+ /** The `Runtime`, from the function or Globals, when the template states one. */
359
+ runtime?: string;
321
360
  httpRoutes: ServerlessHttpRoute[];
322
361
  nonHttpEvents: ServerlessNonHttpEvent[];
323
362
  }
@@ -331,4 +370,4 @@ interface ServerlessFunctionInfo {
331
370
  */
332
371
  declare function readServerlessFunctions(template: CloudFormationTemplate): ServerlessFunctionInfo[];
333
372
 
334
- export { type AppSyncResolverBinding, CLOUDFORMATION_YAML_TAGS, type CloudFormationResource, type CloudFormationTemplate, MAX_STACK_DEPTH, type PatternReduction, type ServerlessFunctionInfo, type ServerlessHttpRoute, type ServerlessNonHttpEvent, type TemplateDocument, type TemplateTree, type UnfollowedReason, type UnfollowedStack, inheritedEnvVars, loadCloudFormationTemplate, loadTemplateTree, qualifiedLogicalId, readAppSyncResolvers, readServerlessFunctions, reduceEventPattern, refTarget, resolveBucketChannel, resolveEventBusToken, resolveQueueChannel, resolveResourceChannel, resolveTopicChannel, resourcesWithGlobals, unfollowedStackMessage };
373
+ export { type AppSyncResolverBinding, CLOUDFORMATION_YAML_TAGS, type CloudFormationResource, type CloudFormationTemplate, type EventPatternFields, type LoadedTemplate, MAX_STACK_DEPTH, type ServerlessFunctionInfo, type ServerlessHttpRoute, type ServerlessNonHttpEvent, type TemplateDocument, type TemplateTree, type UnfollowedReason, type UnfollowedStack, type UnsettledPatternField, apiScopedKey, documentRef, inheritedEnvVars, loadCloudFormationDocument, loadCloudFormationTemplate, loadTemplateTree, qualifiedLogicalId, readAppSyncResolvers, readEventPattern, readResourceLines, readServerlessFunctions, refTarget, resolveBucketChannel, resolveEventBusToken, resolveQueueChannel, resolveResourceChannel, resolveTopicChannel, resourcesWithGlobals, unfollowedStackMessage };
package/dist/index.d.ts CHANGED
@@ -35,6 +35,19 @@ declare const CLOUDFORMATION_YAML_TAGS: YAML.CollectionTag[];
35
35
  * malformed manifest is a load-time error rather than an empty result.
36
36
  */
37
37
  declare function loadCloudFormationTemplate(templatePath: string): CloudFormationTemplate;
38
+ interface LoadedTemplate {
39
+ template: CloudFormationTemplate;
40
+ /** The 1-based line each resource's logical id is written on. */
41
+ resourceLines: Map<string, number>;
42
+ }
43
+ /** `loadCloudFormationTemplate`, plus the line each resource starts on. */
44
+ declare function loadCloudFormationDocument(templatePath: string): LoadedTemplate;
45
+ /**
46
+ * The line each key under `Resources` is written on. JSON is valid YAML,
47
+ * so one reader covers both formats. A text that does not parse gives
48
+ * an empty map, and its summaries keep line 1.
49
+ */
50
+ declare function readResourceLines(raw: string): Map<string, number>;
38
51
  /**
39
52
  * CloudFormation references come out of the parser in four forms:
40
53
  * - { Ref: "LogicalId" }
@@ -81,6 +94,8 @@ interface AppSyncResolverBinding {
81
94
  lambdaFunctionLogicalIds: string[];
82
95
  }
83
96
  declare function readAppSyncResolvers(template: CloudFormationTemplate): AppSyncResolverBinding[];
97
+ /** A name only means something next to the API it was declared in. */
98
+ declare function apiScopedKey(apiId: string | null, name: string): string;
84
99
 
85
100
  /**
86
101
  * Resolves the references AWS manifests use to point at a queue, a topic,
@@ -128,25 +143,32 @@ declare function resolveBucketChannel(value: unknown): string | null;
128
143
  * CloudFormation template and in a serverless.yml eventBridge event, so
129
144
  * both readers reduce them here.
130
145
  */
146
+ /** A pattern field that is not a plain list of strings. */
147
+ type UnsettledPatternField = "detail-type" | "source";
131
148
  /**
132
- * How far a rule's EventPattern reduced: either to the exact
133
- * detail-types it lists, or not at all, along with a reason someone can
134
- * read.
149
+ * What a rule's EventPattern says about the two fields a send states.
150
+ * A field the pattern leaves out matches every value, so it has no
151
+ * entry here. A field written as anything but a list of strings, such
152
+ * as a prefix filter, is listed in `unsettled`.
135
153
  */
136
- type PatternReduction = {
137
- kind: "exact";
138
- detailTypes: string[];
139
- } | {
140
- kind: "unresolvable";
141
- reason: string;
142
- };
143
- /**
144
- * Reduce a rule's EventPattern to the exact set of DetailTypes it
145
- * matches. Only a literal `detail-type` array reduces. Anything else,
146
- * such as a missing detail-type, a content filter or an empty array,
147
- * comes back unresolvable with a reason, and the checker reports it.
148
- */
149
- declare function reduceEventPattern(pattern: unknown): PatternReduction;
154
+ interface EventPatternFields {
155
+ detailTypes?: string[];
156
+ sources?: string[];
157
+ unsettled: UnsettledPatternField[];
158
+ /**
159
+ * The pattern also filters on a field a send does not state, such as
160
+ * `detail`, so a send that agrees on source and detail-type still
161
+ * reaches the rule only for some events.
162
+ */
163
+ filtersOtherFields: boolean;
164
+ /**
165
+ * A source starts with `aws.`. Only AWS services send those, and
166
+ * PutEvents refuses them from anyone else.
167
+ */
168
+ emittedByAws: boolean;
169
+ }
170
+ /** Null when the rule declares no EventPattern at all. */
171
+ declare function readEventPattern(pattern: unknown): EventPatternFields | null;
150
172
  /**
151
173
  * Resolve a rule's EventBusName to the channel's bus token: the CFN
152
174
  * logical id when it's a Ref/GetAtt, the name segment of an event-bus
@@ -217,7 +239,22 @@ interface TemplateDocument {
217
239
  */
218
240
  stackPath: string[];
219
241
  template: CloudFormationTemplate;
242
+ /** The 1-based line each resource's logical id is written on. */
243
+ resourceLines: Map<string, number>;
244
+ /**
245
+ * The resource each of this document's parameters points at, for a
246
+ * parameter the parent stack sets to a reference. The id is qualified
247
+ * by its own document's stack path, so a child's `!Ref ApiId` can be
248
+ * read as the API its parent declares. Empty for the root.
249
+ */
250
+ parameterTargets: Map<string, string>;
220
251
  }
252
+ /**
253
+ * The qualified logical id a reference in this document points at: the
254
+ * resource the parent passed in as a parameter, or the document's own.
255
+ * Null for a value only known at deploy time.
256
+ */
257
+ declare function documentRef(document: Pick<TemplateDocument, "stackPath" | "parameterTargets">, value: unknown): string | null;
221
258
  /**
222
259
  * Why a child template was not read. Every value is a limit on what the
223
260
  * reader could reach, so a consumer should report it as a read that
@@ -309,15 +346,17 @@ interface ServerlessFunctionInfo {
309
346
  logicalId: string;
310
347
  /** Raw Handler string, e.g. "src/handlers/confirmToken.handler". */
311
348
  handler: string;
312
- /** Module-path portion of the handler (everything before the final dot). */
349
+ /** Module-path portion of the handler, as `parseHandler` splits it. */
313
350
  modulePath: string;
314
- /** Exported symbol the handler points at (everything after the final dot). */
351
+ /** The function the handler points at, as `parseHandler` splits it. */
315
352
  exportName: string;
316
353
  /**
317
354
  * CodeUri base directory the handler resolves against: the function's
318
355
  * own CodeUri, else the `Globals.Function.CodeUri` default, else ".".
319
356
  */
320
357
  codeUri: string;
358
+ /** The `Runtime`, from the function or Globals, when the template states one. */
359
+ runtime?: string;
321
360
  httpRoutes: ServerlessHttpRoute[];
322
361
  nonHttpEvents: ServerlessNonHttpEvent[];
323
362
  }
@@ -331,4 +370,4 @@ interface ServerlessFunctionInfo {
331
370
  */
332
371
  declare function readServerlessFunctions(template: CloudFormationTemplate): ServerlessFunctionInfo[];
333
372
 
334
- export { type AppSyncResolverBinding, CLOUDFORMATION_YAML_TAGS, type CloudFormationResource, type CloudFormationTemplate, MAX_STACK_DEPTH, type PatternReduction, type ServerlessFunctionInfo, type ServerlessHttpRoute, type ServerlessNonHttpEvent, type TemplateDocument, type TemplateTree, type UnfollowedReason, type UnfollowedStack, inheritedEnvVars, loadCloudFormationTemplate, loadTemplateTree, qualifiedLogicalId, readAppSyncResolvers, readServerlessFunctions, reduceEventPattern, refTarget, resolveBucketChannel, resolveEventBusToken, resolveQueueChannel, resolveResourceChannel, resolveTopicChannel, resourcesWithGlobals, unfollowedStackMessage };
373
+ export { type AppSyncResolverBinding, CLOUDFORMATION_YAML_TAGS, type CloudFormationResource, type CloudFormationTemplate, type EventPatternFields, type LoadedTemplate, MAX_STACK_DEPTH, type ServerlessFunctionInfo, type ServerlessHttpRoute, type ServerlessNonHttpEvent, type TemplateDocument, type TemplateTree, type UnfollowedReason, type UnfollowedStack, type UnsettledPatternField, apiScopedKey, documentRef, inheritedEnvVars, loadCloudFormationDocument, loadCloudFormationTemplate, loadTemplateTree, qualifiedLogicalId, readAppSyncResolvers, readEventPattern, readResourceLines, readServerlessFunctions, refTarget, resolveBucketChannel, resolveEventBusToken, resolveQueueChannel, resolveResourceChannel, resolveTopicChannel, resourcesWithGlobals, unfollowedStackMessage };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/templateLoader.ts
2
2
  import fs from "fs";
3
3
  import path from "path";
4
- import YAML from "yaml";
4
+ import YAML, { isMap, isScalar, LineCounter } from "yaml";
5
5
  var everyNodeKind = (tag, resolve) => {
6
6
  const forNode = (value) => resolve(plainly(value));
7
7
  return [
@@ -44,6 +44,9 @@ var CLOUDFORMATION_YAML_TAGS = [
44
44
  ].flatMap((tag) => everyNodeKind(tag, (value) => value))
45
45
  ];
46
46
  function loadCloudFormationTemplate(templatePath) {
47
+ return loadCloudFormationDocument(templatePath).template;
48
+ }
49
+ function loadCloudFormationDocument(templatePath) {
47
50
  const resolved = path.resolve(templatePath);
48
51
  if (!fs.existsSync(resolved)) {
49
52
  throw new Error(`CloudFormation template not found: ${resolved}`);
@@ -54,7 +57,30 @@ function loadCloudFormationTemplate(templatePath) {
54
57
  if (parsed === null || typeof parsed !== "object") {
55
58
  throw new Error(`CloudFormation template is not an object: ${resolved}`);
56
59
  }
57
- return parsed;
60
+ return {
61
+ template: parsed,
62
+ resourceLines: readResourceLines(raw)
63
+ };
64
+ }
65
+ function readResourceLines(raw) {
66
+ const lines = /* @__PURE__ */ new Map();
67
+ const lineCounter = new LineCounter();
68
+ const document = YAML.parseDocument(raw, {
69
+ customTags: CLOUDFORMATION_YAML_TAGS,
70
+ lineCounter
71
+ });
72
+ const resources = document.get("Resources", true);
73
+ if (!isMap(resources)) {
74
+ return lines;
75
+ }
76
+ for (const item of resources.items) {
77
+ const key = item.key;
78
+ if (!isScalar(key) || key.range === void 0 || key.range === null) {
79
+ continue;
80
+ }
81
+ lines.set(String(key.value), lineCounter.linePos(key.range[0]).line);
82
+ }
83
+ return lines;
58
84
  }
59
85
  function refTarget(value) {
60
86
  if (typeof value === "string") {
@@ -279,40 +305,36 @@ function resolveBucketChannel(value) {
279
305
  }
280
306
 
281
307
  // src/eventPattern.ts
282
- function reduceEventPattern(pattern) {
308
+ var STATED_FIELDS = /* @__PURE__ */ new Set(["detail-type", "source"]);
309
+ function readEventPattern(pattern) {
283
310
  if (pattern === null || typeof pattern !== "object") {
284
- return {
285
- kind: "unresolvable",
286
- reason: "rule declares no EventPattern"
287
- };
288
- }
289
- const detailType = pattern["detail-type"];
290
- if (detailType === void 0) {
291
- return {
292
- kind: "unresolvable",
293
- reason: "EventPattern has no detail-type; v0 reduces routing to exact detail-type match only"
294
- };
295
- }
296
- if (!Array.isArray(detailType)) {
297
- return {
298
- kind: "unresolvable",
299
- reason: "detail-type is not a plain array of string literals"
300
- };
311
+ return null;
301
312
  }
302
- const detailTypes = [];
303
- for (const entry of detailType) {
304
- if (typeof entry !== "string") {
305
- return {
306
- kind: "unresolvable",
307
- reason: "detail-type contains a content filter (prefix / anything-but / etc.); pattern subsumption is out of v0 scope"
308
- };
309
- }
310
- detailTypes.push(entry);
313
+ const record = pattern;
314
+ const detailTypes = literalValues(record["detail-type"]);
315
+ const sources = literalValues(record.source);
316
+ const unsettled = [
317
+ ...detailTypes === "unsettled" ? ["detail-type"] : [],
318
+ ...sources === "unsettled" ? ["source"] : []
319
+ ];
320
+ return {
321
+ ...Array.isArray(detailTypes) ? { detailTypes } : {},
322
+ ...Array.isArray(sources) ? { sources } : {},
323
+ unsettled,
324
+ filtersOtherFields: Object.keys(record).some(
325
+ (key) => !STATED_FIELDS.has(key)
326
+ ),
327
+ emittedByAws: Array.isArray(sources) && sources.some((source) => source.startsWith("aws."))
328
+ };
329
+ }
330
+ function literalValues(value) {
331
+ if (value === void 0) {
332
+ return null;
311
333
  }
312
- if (detailTypes.length === 0) {
313
- return { kind: "unresolvable", reason: "detail-type array is empty" };
334
+ if (!Array.isArray(value) || value.length === 0 || !value.every((entry) => typeof entry === "string")) {
335
+ return "unsettled";
314
336
  }
315
- return { kind: "exact", detailTypes };
337
+ return value;
316
338
  }
317
339
  function resolveEventBusToken(value) {
318
340
  if (value === null || value === void 0) {
@@ -431,12 +453,40 @@ var CHILD_TEMPLATE_PROPERTY = {
431
453
  "AWS::Serverless::Application": "Location"
432
454
  };
433
455
  var MAX_STACK_DEPTH = 10;
456
+ function documentRef(document, value) {
457
+ const id = refTarget(value);
458
+ if (id === null) {
459
+ return null;
460
+ }
461
+ return document.parameterTargets.get(id) ?? qualifiedLogicalId(document.stackPath, id);
462
+ }
463
+ function parameterTargetsOf(parent, passed) {
464
+ const targets = /* @__PURE__ */ new Map();
465
+ if (passed === null || typeof passed !== "object") {
466
+ return targets;
467
+ }
468
+ const resources = parent.template.Resources ?? {};
469
+ for (const [name, value] of Object.entries(
470
+ passed
471
+ )) {
472
+ const id = refTarget(value);
473
+ if (id === null || typeof value === "string") {
474
+ continue;
475
+ }
476
+ const target = parent.parameterTargets.get(id) ?? (resources[id] === void 0 ? null : qualifiedLogicalId(parent.stackPath, id));
477
+ if (target !== null) {
478
+ targets.set(name, target);
479
+ }
480
+ }
481
+ return targets;
482
+ }
434
483
  function loadTemplateTree(rootPath) {
435
484
  const rootFile = path2.resolve(rootPath);
436
485
  const root = {
437
486
  path: rootFile,
438
487
  stackPath: [],
439
- template: loadCloudFormationTemplate(rootFile)
488
+ ...loadCloudFormationDocument(rootFile),
489
+ parameterTargets: /* @__PURE__ */ new Map()
440
490
  };
441
491
  const documents = [root];
442
492
  const unfollowed = [];
@@ -480,7 +530,8 @@ function childStacksOf(document, ancestors) {
480
530
  document,
481
531
  ancestors,
482
532
  logicalId,
483
- location: resource.Properties?.[property]
533
+ location: resource.Properties?.[property],
534
+ parameters: resource.Properties?.Parameters
484
535
  })
485
536
  );
486
537
  }
@@ -542,7 +593,8 @@ function childStack(opts) {
542
593
  document: {
543
594
  path: childPath,
544
595
  stackPath,
545
- template: loadCloudFormationTemplate(childPath)
596
+ ...loadCloudFormationDocument(childPath),
597
+ parameterTargets: parameterTargetsOf(opts.document, opts.parameters)
546
598
  }
547
599
  };
548
600
  } catch (err) {
@@ -613,7 +665,9 @@ function readServerlessFunctions(template) {
613
665
  if (typeof handlerRaw !== "string") {
614
666
  continue;
615
667
  }
616
- const parsed = parseHandler(handlerRaw);
668
+ const runtimeRaw = resource.Properties?.Runtime;
669
+ const runtime = typeof runtimeRaw === "string" ? runtimeRaw : void 0;
670
+ const parsed = parseHandler(handlerRaw, runtime);
617
671
  if (parsed === null) {
618
672
  continue;
619
673
  }
@@ -626,6 +680,7 @@ function readServerlessFunctions(template) {
626
680
  modulePath: parsed.modulePath,
627
681
  exportName: parsed.exportName,
628
682
  codeUri: readCodeUri(resource),
683
+ ...runtime !== void 0 ? { runtime } : {},
629
684
  httpRoutes,
630
685
  nonHttpEvents
631
686
  });
@@ -635,14 +690,18 @@ function readServerlessFunctions(template) {
635
690
  export {
636
691
  CLOUDFORMATION_YAML_TAGS,
637
692
  MAX_STACK_DEPTH,
693
+ apiScopedKey,
694
+ documentRef,
638
695
  inheritedEnvVars,
696
+ loadCloudFormationDocument,
639
697
  loadCloudFormationTemplate,
640
698
  loadTemplateTree,
641
699
  parseHandler2 as parseHandler,
642
700
  qualifiedLogicalId,
643
701
  readAppSyncResolvers,
702
+ readEventPattern,
703
+ readResourceLines,
644
704
  readServerlessFunctions,
645
- reduceEventPattern,
646
705
  refTarget,
647
706
  resolveBucketChannel,
648
707
  resolveEventBusToken,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/manifest-aws",
3
- "version": "0.35.1",
3
+ "version": "0.37.0",
4
4
  "type": "module",
5
5
  "description": "Parse a CloudFormation or SAM template into plain data, the facts every suss reader of an AWS template works from.",
6
6
  "keywords": [
@@ -46,7 +46,7 @@
46
46
  "typecheck": "tsc --noEmit"
47
47
  },
48
48
  "dependencies": {
49
- "@suss/ir-core": "0.35.1",
49
+ "@suss/ir-core": "0.37.0",
50
50
  "yaml": "^2.4.0"
51
51
  },
52
52
  "devDependencies": {